Browse Source

邮件服务接口增加和调整,业务拆分

1811872455@163.com 3 weeks ago
parent
commit
bfd0230198
27 changed files with 853 additions and 638 deletions
  1. 3 4
      storlead-api/src/main/java/com/storlead/mail/ThumbnailPreviewController.java
  2. 0 4
      storlead-api/src/main/resources/application-dev.yml
  3. 0 4
      storlead-api/src/main/resources/application-prod.yml
  4. 0 4
      storlead-api/src/main/resources/application-test.yml
  5. 4 25
      storlead-api/src/main/resources/application.yml
  6. 0 7
      storlead-framework/storlead-common/pom.xml
  7. 0 33
      storlead-framework/storlead-common/src/main/java/com/storlead/framework/common/config/AutowiringSpringBeanJobFactory.java
  8. 0 50
      storlead-framework/storlead-common/src/main/java/com/storlead/framework/common/config/QuartzConfig.java
  9. 113 113
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/datasource/ModuleDefaultDataSourceAspect.java
  10. 50 0
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/entity/SysBaseField.java
  11. 31 30
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/handler/MyMetaObjectHandler.java
  12. 30 7
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/interceptor/MybatisPlusConfig.java
  13. 17 0
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/service/MyBaseService.java
  14. 105 18
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/service/impl/MyBaseServiceImpl.java
  15. 4 16
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/tenant/StorleadTenantHandler.java
  16. 77 2
      storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/tenant/TenantProperties.java
  17. 89 68
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/config/Swagger2Config.java
  18. 57 57
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/filter/AuthRequestFilter.java
  19. 1 1
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/interceptor/WebConfig.java
  20. 1 1
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/message/WebResp.java
  21. 45 12
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/process/ApiResultHandler.java
  22. 26 0
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/process/StorleadFastJsonHttpMessageConverter.java
  23. 52 72
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/process/WebMvcConfigurer.java
  24. 55 0
      storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/properties/FileProperties.java
  25. 18 31
      storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/AutoSendDelayEmailTaskJob.java
  26. 65 59
      storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/DelayDeleteServerEmailTaskJob.java
  27. 10 20
      storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/EmailPullHeadContentTaskJob.java

+ 3 - 4
storlead-api/src/main/java/com/storlead/mail/ThumbnailPreviewController.java

@@ -1,16 +1,15 @@
 package com.storlead.mail;
 
-import com.storlead.system.properties.FileProperties;
+import com.storlead.framework.web.properties.FileProperties;
 import io.swagger.annotations.Api;
 import lombok.extern.log4j.Log4j2;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
-
+import javax.annotation.Resource;
 import javax.imageio.IIOImage;
 import javax.imageio.ImageIO;
 import javax.imageio.ImageWriteParam;
@@ -34,7 +33,7 @@ import java.util.Iterator;
 @Log4j2
 public class ThumbnailPreviewController {
 
-    @Autowired
+    @Resource
     private FileProperties fileProperties;
 
     @GetMapping("/thumbnail")

+ 0 - 4
storlead-api/src/main/resources/application-dev.yml

@@ -21,10 +21,6 @@ spring:
     multipart:
       max-file-size: 20MB
       max-request-size: 20MB
-  ## quartz定时任务,采用数据库方式
-  #  quartz:
-  #    job-store-type: jdbc
-  #json 时间戳统一转换
   jackson:
     date-format:   yyyy-MM-dd HH:mm:ss
     time-zone:   GMT+8

+ 0 - 4
storlead-api/src/main/resources/application-prod.yml

@@ -23,10 +23,6 @@ spring:
     multipart:
       max-file-size: 20MB
       max-request-size: 20MB
-  ## quartz定时任务,采用数据库方式
-  #  quartz:
-  #    job-store-type: jdbc
-  #json 时间戳统一转换
   jackson:
     date-format:   yyyy-MM-dd HH:mm:ss
     time-zone:   GMT+8

+ 0 - 4
storlead-api/src/main/resources/application-test.yml

@@ -21,10 +21,6 @@ spring:
     multipart:
       max-file-size: 20MB
       max-request-size: 20MB
-  ## quartz定时任务,采用数据库方式
-  #  quartz:
-  #    job-store-type: jdbc
-  #json 时间戳统一转换
   jackson:
     date-format:   yyyy-MM-dd HH:mm:ss
     time-zone:   GMT+8

+ 4 - 25
storlead-api/src/main/resources/application.yml

@@ -15,31 +15,6 @@ spring:
   main:
     allow-circular-references: true
     allow-bean-definition-overriding: true
-  # 定时任务
-  quartz:
-    jdbc:
-      initialize-schema: never
-    job-store-type: jdbc
-    properties:
-      org:
-        quartz:
-          scheduler:
-            instanceId: AUTO
-            instanceName: clusteredScheduler
-          jobStore:
-            useProperties: true
-            isClustered: true
-            maxMisfiresToHandleAtATime: 1
-            tablePrefix: qrtz_
-   #         class: org.quartz.impl.jdbcjobstore.LocalDataSourceJobStore
-            class: org.quartz.impl.jdbcjobstore.JobStoreTX
-            clusterCheckinInterval: 3600000
-            driverDelegateClass: org.quartz.impl.jdbcjobstore.StdJDBCDelegate
-          threadPool:
-            threadCount: 10
-            threadPriority: 5
-            threadsInheritContextClassLoaderOfInitializingThread: true
-            class: org.quartz.simpl.SimpleThreadPool
   # 在上下文中没有Executor bean的情况下,Spring Boot会自动配置ThreadPoolTaskExecutor,并使用合理的默认值,这些默认值可以自动关联到异步任务执行(@EnableAsync)和Spring MVC异步请求处理。
   #如果您已经在上下文中定义了一个自定义执行器,那么常规任务执行(即@EnableAsync)将透明地使用它,但是Spring MVC支持将不会被配置,因为它需要一个AsyncTaskExecutor实现(名为applicationTaskExecutor)。根据您的目标安排,您可以将执行程序更改为ThreadPoolTaskExecutor,或者定义一个ThreadPoolTaskExecutor和一个包装自定义执行程序的AsyncConfigurer。
   task:
@@ -74,6 +49,10 @@ storlead:
       batch-size: 20
       # 单体部署且 CRM/消息 Bean 在本进程时 true;仅落库由外部消费时 false
       invoke-external-beans: true
+    scheduler:
+      pull-email-cron: "0 */5 * * * ?"
+      delay-send-cron: "0 * * * * ?"
+      delay-delete-cron: "0 0 2 * * ?"
   datasource:
     # 邮件服务默认走主库 master(未标注 @DS 时生效;标注 @DS 则以注解为准)
     module-default-enabled: true

+ 0 - 7
storlead-framework/storlead-common/pom.xml

@@ -196,13 +196,6 @@
             <version>2.3.32</version>
         </dependency>
 
-        <dependency>
-            <groupId>org.springframework.boot</groupId>
-            <artifactId>spring-boot-starter-quartz</artifactId>
-            <version>2.7.0</version>
-        </dependency>
-
-
         <dependency>
             <groupId>com.google.code.gson</groupId>
             <artifactId>gson</artifactId>

+ 0 - 33
storlead-framework/storlead-common/src/main/java/com/storlead/framework/common/config/AutowiringSpringBeanJobFactory.java

@@ -1,33 +0,0 @@
-package com.storlead.framework.common.config;
-
-
-import org.quartz.spi.TriggerFiredBundle;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
-import org.springframework.scheduling.quartz.SpringBeanJobFactory;
-import org.springframework.stereotype.Component;
-
-import javax.annotation.Resource;
-
-/**
- * @program: sp-salary-system
- * @description:
- * @author: chenkq
- * @create: 2024-10-17 09:32
- */
-@Component
-public class AutowiringSpringBeanJobFactory extends SpringBeanJobFactory {
-
-    @Resource
-    private AutowireCapableBeanFactory beanFactory;
-
-    @Override
-    protected Object createJobInstance(TriggerFiredBundle bundle) throws Exception {
-        // 使用父类方法创建 Job 实例
-        Object jobInstance = super.createJobInstance(bundle);
-        // 将 Spring 管理的 Bean 注入到 Job 实例中
-        beanFactory.autowireBean(jobInstance);
-        return jobInstance;
-    }
-}
-

+ 0 - 50
storlead-framework/storlead-common/src/main/java/com/storlead/framework/common/config/QuartzConfig.java

@@ -1,50 +0,0 @@
-package com.storlead.framework.common.config;
-
-import lombok.extern.log4j.Log4j2;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.scheduling.quartz.SchedulerFactoryBean;
-
-import javax.annotation.Resource;
-import javax.sql.DataSource;
-import java.util.Properties;
-
-/**
- * @program: sp-salary-system
- * @description:
- * @author: chenkq
- * @create: 2024-01-03 18:06
- */
-@Log4j2
-@Configuration
-public class QuartzConfig {
-
-    @Resource
-    private DataSource dataSource;  // Spring Boot 自动配置的数据源
-
-    @Resource
-    private AutowiringSpringBeanJobFactory jobFactory;
-
-    @Bean
-    public SchedulerFactoryBean schedulerFactoryBean() {
-        SchedulerFactoryBean factory = new SchedulerFactoryBean();
-        factory.setDataSource(dataSource);  // 显式地设置 Quartz 使用的数据源
-        factory.setJobFactory(jobFactory);
-
-        Properties quartzProperties = new Properties();
-        quartzProperties.put("org.quartz.scheduler.instanceId", "AUTO");
-        quartzProperties.put("org.quartz.scheduler.instanceName", "clusteredScheduler");
-        quartzProperties.put("org.quartz.jobStore.isClustered", "true");
-        quartzProperties.put("org.quartz.jobStore.useProperties", "true");
-        quartzProperties.put("org.quartz.jobStore.clusterCheckinInterval", "60000");
-        quartzProperties.put("org.quartz.jobStore.driverDelegateClass", "org.quartz.impl.jdbcjobstore.StdJDBCDelegate");
-        quartzProperties.put("org.quartz.jobStore.tablePrefix", "qrtz_");
-        quartzProperties.put("org.quartz.threadPool.threadCount", "10");
-        quartzProperties.put("org.quartz.threadPool.threadPriority", "5");
-        quartzProperties.put("org.quartz.threadPool.threadsInheritContextClassLoaderOfInitializingThread", "false");
-        factory.setQuartzProperties(quartzProperties);
-
-        return factory;
-    }
-}

+ 113 - 113
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/datasource/ModuleDefaultDataSourceAspect.java

@@ -1,113 +1,113 @@
-package com.storlead.framework.mybatis.datasource;
-
-import com.baomidou.dynamic.datasource.annotation.DS;
-import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
-import lombok.RequiredArgsConstructor;
-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.core.annotation.AnnotatedElementUtils;
-import org.springframework.core.annotation.Order;
-import org.springframework.stereotype.Component;
-import org.springframework.util.StringUtils;
-
-import java.lang.reflect.Method;
-
-/**
- * 子模块默认数据源:按配置包前缀 push 数据源;若目标已标注 {@link DS} 则跳过,由官方 {@code @DS} 切面处理。
- */
-@Slf4j
-@Aspect
-@Component
-@Order(0)
-@RequiredArgsConstructor
-public class ModuleDefaultDataSourceAspect {
-
-    private final ModuleDefaultDataSourceProperties properties;
-
-    @Pointcut("execution(* *(..)) && (within(com.storlead..service..*) || within(com.storlead..mapper..*))")
-    public void moduleServiceOrMapperLayer() {
-    }
-
-    @Around("moduleServiceOrMapperLayer()")
-    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
-        if (!properties.isModuleDefaultEnabled()) {
-            return joinPoint.proceed();
-        }
-        if (hasDsAnnotation(joinPoint)) {
-            return joinPoint.proceed();
-        }
-        String datasource = resolveDatasource(joinPoint);
-        if (!StringUtils.hasText(datasource)) {
-            return joinPoint.proceed();
-        }
-        DynamicDataSourceContextHolder.push(datasource);
-        try {
-            if (log.isDebugEnabled()) {
-                log.debug("module default datasource [{}] -> {}", datasource, joinPoint.getSignature().toShortString());
-            }
-            return joinPoint.proceed();
-        } finally {
-            DynamicDataSourceContextHolder.poll();
-        }
-    }
-
-    private boolean hasDsAnnotation(ProceedingJoinPoint joinPoint) {
-        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
-        Method method = signature.getMethod();
-        Class<?> declaringType = signature.getDeclaringType();
-        if (AnnotatedElementUtils.hasAnnotation(method, DS.class)) {
-            return true;
-        }
-        if (declaringType != null && AnnotatedElementUtils.hasAnnotation(declaringType, DS.class)) {
-            return true;
-        }
-        return AnnotatedElementUtils.hasAnnotation(joinPoint.getTarget().getClass(), DS.class);
-    }
-
-    /**
-     * 最长包前缀匹配
-     */
-    private String resolveDatasource(ProceedingJoinPoint joinPoint) {
-        String className = resolveBusinessClassName(joinPoint);
-        String matchedDs = null;
-        int matchedLen = -1;
-        for (ModuleDefaultDataSourceProperties.ModuleDefaultRule rule : properties.getModuleDefaults()) {
-            if (rule.getPackages() == null || !StringUtils.hasText(rule.getDatasource())) {
-                continue;
-            }
-            for (String pkg : rule.getPackages()) {
-                if (!StringUtils.hasText(pkg)) {
-                    continue;
-                }
-                if (matchesPackage(className, pkg.trim()) && pkg.length() > matchedLen) {
-                    matchedLen = pkg.length();
-                    matchedDs = rule.getDatasource();
-                }
-            }
-        }
-        return matchedDs;
-    }
-
-    private boolean matchesPackage(String className, String packagePrefix) {
-        return className.equals(packagePrefix)
-                || className.startsWith(packagePrefix + ".");
-    }
-
-    /**
-     * Mapper 为 JDK 代理时,用接口全限定名匹配包前缀。
-     */
-    private String resolveBusinessClassName(ProceedingJoinPoint joinPoint) {
-        if (joinPoint.getSignature() instanceof MethodSignature) {
-            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
-            Class<?> declaringType = signature.getDeclaringType();
-            if (declaringType != null && declaringType.getName().startsWith("com.storlead.")) {
-                return declaringType.getName();
-            }
-        }
-        return joinPoint.getTarget().getClass().getName();
-    }
-}
+package com.storlead.framework.mybatis.datasource;//package com.storlead.framework.mybatis.datasource;
+//
+//import com.baomidou.dynamic.datasource.annotation.DS;
+//import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
+//import lombok.RequiredArgsConstructor;
+//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.core.annotation.AnnotatedElementUtils;
+//import org.springframework.core.annotation.Order;
+//import org.springframework.stereotype.Component;
+//import org.springframework.util.StringUtils;
+//
+//import java.lang.reflect.Method;
+//
+///**
+// * 子模块默认数据源:按配置包前缀 push 数据源;若目标已标注 {@link DS} 则跳过,由官方 {@code @DS} 切面处理。
+// */
+//@Slf4j
+//@Aspect
+//@Component
+//@Order(0)
+//@RequiredArgsConstructor
+//public class ModuleDefaultDataSourceAspect {
+//
+//    private final ModuleDefaultDataSourceProperties properties;
+//
+//    @Pointcut("execution(* *(..)) && (within(com.storlead..service..*) || within(com.storlead..mapper..*))")
+//    public void moduleServiceOrMapperLayer() {
+//    }
+//
+//    @Around("moduleServiceOrMapperLayer()")
+//    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
+//        if (!properties.isModuleDefaultEnabled()) {
+//            return joinPoint.proceed();
+//        }
+//        if (hasDsAnnotation(joinPoint)) {
+//            return joinPoint.proceed();
+//        }
+//        String datasource = resolveDatasource(joinPoint);
+//        if (!StringUtils.hasText(datasource)) {
+//            return joinPoint.proceed();
+//        }
+//        DynamicDataSourceContextHolder.push(datasource);
+//        try {
+//            if (log.isDebugEnabled()) {
+//                log.debug("module default datasource [{}] -> {}", datasource, joinPoint.getSignature().toShortString());
+//            }
+//            return joinPoint.proceed();
+//        } finally {
+//            DynamicDataSourceContextHolder.poll();
+//        }
+//    }
+//
+//    private boolean hasDsAnnotation(ProceedingJoinPoint joinPoint) {
+//        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+//        Method method = signature.getMethod();
+//        Class<?> declaringType = signature.getDeclaringType();
+//        if (AnnotatedElementUtils.hasAnnotation(method, DS.class)) {
+//            return true;
+//        }
+//        if (declaringType != null && AnnotatedElementUtils.hasAnnotation(declaringType, DS.class)) {
+//            return true;
+//        }
+//        return AnnotatedElementUtils.hasAnnotation(joinPoint.getTarget().getClass(), DS.class);
+//    }
+//
+//    /**
+//     * 最长包前缀匹配
+//     */
+//    private String resolveDatasource(ProceedingJoinPoint joinPoint) {
+//        String className = resolveBusinessClassName(joinPoint);
+//        String matchedDs = null;
+//        int matchedLen = -1;
+//        for (ModuleDefaultDataSourceProperties.ModuleDefaultRule rule : properties.getModuleDefaults()) {
+//            if (rule.getPackages() == null || !StringUtils.hasText(rule.getDatasource())) {
+//                continue;
+//            }
+//            for (String pkg : rule.getPackages()) {
+//                if (!StringUtils.hasText(pkg)) {
+//                    continue;
+//                }
+//                if (matchesPackage(className, pkg.trim()) && pkg.length() > matchedLen) {
+//                    matchedLen = pkg.length();
+//                    matchedDs = rule.getDatasource();
+//                }
+//            }
+//        }
+//        return matchedDs;
+//    }
+//
+//    private boolean matchesPackage(String className, String packagePrefix) {
+//        return className.equals(packagePrefix)
+//                || className.startsWith(packagePrefix + ".");
+//    }
+//
+//    /**
+//     * Mapper 为 JDK 代理时,用接口全限定名匹配包前缀。
+//     */
+//    private String resolveBusinessClassName(ProceedingJoinPoint joinPoint) {
+//        if (joinPoint.getSignature() instanceof MethodSignature) {
+//            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
+//            Class<?> declaringType = signature.getDeclaringType();
+//            if (declaringType != null && declaringType.getName().startsWith("com.storlead.")) {
+//                return declaringType.getName();
+//            }
+//        }
+//        return joinPoint.getTarget().getClass().getName();
+//    }
+//}

+ 50 - 0
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/entity/SysBaseField.java

@@ -64,6 +64,56 @@ public class SysBaseField implements Serializable {
     private Integer sort;
 
 
+    public static  <T>  void dictFileds(List<T> tList) {
+        if (CollectionUtils.isEmpty(tList)) {
+            return;
+        }
+//        for (T t : tList) {
+        try {
+            long be = System.currentTimeMillis();
+            log.debug("dictUtils---convertDict");
+            Object myClassInstance = SpringContextUtils.getBean("dictUtils");
+            Class<?> clazz = myClassInstance.getClass();
+            Method method = clazz.getMethod("convertDictls",List.class);
+            // 调用未编写的方法
+            method.invoke(myClassInstance,tList);
+            log.debug("-------------------耗时 ==: "+(System.currentTimeMillis() - be));
+        } catch (Exception e1) {
+            log.error("方法不存在",e1);
+        }
+//        }
+    }
+
+    public void dictFiled() {
+        dictFileds(Arrays.asList(this));
+    }
+
+    public static void dictFiled(Object t) {
+        dictFileds(Arrays.asList(t));
+    }
+
+    public static  <T> void orgFileds(Object... vars) {
+        orgFileds(Arrays.asList(vars));
+    }
+    public static  <T>  void orgFileds(List<T> tList) {
+        if (CollectionUtils.isEmpty(tList)) {
+            return;
+        }
+        try {
+            log.debug("dictUtils---convertDict");
+            Object myClassInstance = SpringContextUtils.getBean("orgAnnotateConvertUtils");
+            Class<?> clazz = myClassInstance.getClass();
+            Method method = clazz.getMethod("convertOrg",List.class);
+            // 调用未编写的方法
+            method.invoke(myClassInstance,tList);
+        } catch (Exception e1) {
+            log.error("方法不存在",e1);
+        }
+    }
+    @SneakyThrows
+    public void orgFiled() {
+        orgFileds(Arrays.asList(this));
+    }
 
     // Method to set empty strings to null
     public void setEmptyStringsToNull() {

+ 31 - 30
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/handler/MyMetaObjectHandler.java

@@ -1,44 +1,41 @@
 package com.storlead.framework.mybatis.handler;
 
 import cn.hutool.core.date.DateTime;
+import com.baomidou.mybatisplus.annotation.TableName;
 import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
 import com.storlead.framework.auth.vo.LoginUser;
 import com.storlead.framework.common.constant.UserCacheKeyConstants;
 import com.storlead.framework.core.context.Context;
 import com.storlead.framework.core.tenant.TenantContext;
+import com.storlead.framework.mybatis.tenant.TenantProperties;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.ibatis.reflection.MetaObject;
 
 import java.util.Objects;
 
 /**
- * @program: sp-sales
- * @description:
- * @author: chenkq
- * @create: 2022-07-06 15:03
+ * 公共字段自动填充;tenant_id 仅对白名单表且实体含 tenantId 字段时写入。
  */
 @Slf4j
 public class MyMetaObjectHandler implements MetaObjectHandler {
 
-    /**
-     * 自动更新
-     * 需要  @TableField(fill = FieldFill.INSERT) 否则无效
-     *
-     * @param metaObject 要处理的对象
-     * @author chenkq
-     * @date 2022-2-3 12:25
-     */
+    private final TenantProperties tenantProperties;
+
+    public MyMetaObjectHandler(TenantProperties tenantProperties) {
+        this.tenantProperties = tenantProperties;
+    }
+
     @Override
     public void insertFill(MetaObject metaObject) {
         Object createTime = getFieldValByName("createTime", metaObject);
         if (Objects.isNull(createTime)) {
-            DateTime time= new DateTime();
-            this.setInsertFieldValByName("createTime",time,metaObject);
+            DateTime time = new DateTime();
+            this.setInsertFieldValByName("createTime", time, metaObject);
         }
         Object updateTime = getFieldValByName("updateTime", metaObject);
         if (Objects.isNull(updateTime)) {
-            DateTime time= new DateTime();
-            this.setUpdateFieldValByName( "updateTime",time, metaObject);
+            DateTime time = new DateTime();
+            this.setUpdateFieldValByName("updateTime", time, metaObject);
         }
         if (Objects.isNull(Context.getContext())) {
             return;
@@ -47,7 +44,7 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
         Object createBy = getFieldValByName("createBy", metaObject);
         if (Objects.isNull(createBy)) {
             if (Objects.nonNull(loginUser)) {
-                this.setInsertFieldValByName("createBy",loginUser.getId(),metaObject);
+                this.setInsertFieldValByName("createBy", loginUser.getId(), metaObject);
             }
         }
         Object ownerBy = getFieldValByName("ownerBy", metaObject);
@@ -56,7 +53,7 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
                 this.setInsertFieldValByName("ownerBy", loginUser.getId(), metaObject);
             }
         }
-        if (metaObject.hasSetter("tenantId")) {
+        if (metaObject.hasSetter("tenantId") && shouldFillTenantId(metaObject)) {
             Object tenantIdVal = getFieldValByName("tenantId", metaObject);
             if (Objects.isNull(tenantIdVal)) {
                 Long tid = TenantContext.getTenantId();
@@ -68,23 +65,15 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
         Object updateBy = getFieldValByName("updateBy", metaObject);
         if (Objects.isNull(updateBy)) {
             if (Objects.nonNull(loginUser)) {
-                this.setInsertFieldValByName("updateBy",loginUser.getId(),metaObject);
+                this.setInsertFieldValByName("updateBy", loginUser.getId(), metaObject);
             }
         }
     }
 
-    /**
-     * 自动更新
-     * 需要  @TableField(fill = FieldFill.UPDATE) 否则无效
-     *
-     * @param metaObject 要处理的对象
-     * @author chenkq
-     * @date 2022-2-3 12:25
-     */
     @Override
     public void updateFill(MetaObject metaObject) {
-        DateTime time= new DateTime();
-        this.setUpdateFieldValByName( "updateTime",time, metaObject);
+        DateTime time = new DateTime();
+        this.setUpdateFieldValByName("updateTime", time, metaObject);
         if (Objects.isNull(Context.getContext())) {
             return;
         }
@@ -92,8 +81,20 @@ public class MyMetaObjectHandler implements MetaObjectHandler {
         LoginUser loginUser = Context.getContext().getAttribute(UserCacheKeyConstants.LOGIN_USER_INFO_KEY, LoginUser.class);
         if (Objects.isNull(updateBy)) {
             if (Objects.nonNull(loginUser)) {
-                this.setInsertFieldValByName("updateBy",loginUser.getId(),metaObject);
+                this.setInsertFieldValByName("updateBy", loginUser.getId(), metaObject);
             }
         }
     }
+
+    private boolean shouldFillTenantId(MetaObject metaObject) {
+        Object original = metaObject.getOriginalObject();
+        if (original == null) {
+            return false;
+        }
+        TableName tableName = original.getClass().getAnnotation(TableName.class);
+        if (tableName == null || tableName.value().isEmpty()) {
+            return false;
+        }
+        return tenantProperties.shouldApplyTenantLine(tableName.value());
+    }
 }

+ 30 - 7
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/interceptor/MybatisPlusConfig.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.core.MybatisConfiguration;
 import com.baomidou.mybatisplus.core.MybatisXMLLanguageDriver;
 import com.baomidou.mybatisplus.core.config.GlobalConfig;
 import com.baomidou.mybatisplus.core.parser.ISqlParser;
+import com.baomidou.mybatisplus.core.parser.ISqlParserFilter;
 import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser;
 import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
@@ -47,11 +48,32 @@ public class MybatisPlusConfig {
     @Bean
     public PaginationInterceptor paginationInterceptor(TenantProperties tenantProperties) {
         PaginationInterceptor interceptor = new PaginationInterceptor().setLimit(-1);
-        TenantSqlParser tenantSqlParser = new TenantSqlParser();
-        tenantSqlParser.setTenantHandler(new StorleadTenantHandler(tenantProperties));
-        List<ISqlParser> sqlParserList = new ArrayList<>();
-        sqlParserList.add(tenantSqlParser);
-        interceptor.setSqlParserList(sqlParserList);
+
+        if (tenantProperties.isEnabled()) {
+            TenantSqlParser tenantSqlParser = new TenantSqlParser();
+            tenantSqlParser.setTenantHandler(new StorleadTenantHandler(tenantProperties));
+            List<ISqlParser> sqlParserList = new ArrayList<>();
+            sqlParserList.add(tenantSqlParser);
+            interceptor.setSqlParserList(sqlParserList);
+            log.info("MyBatis tenant line enabled, includeTables={}", tenantProperties.getIncludeTables());
+        } else {
+            log.info("MyBatis tenant line disabled (storlead.tenant.enabled=false)");
+        }
+
+        interceptor.setSqlParserFilter(new ISqlParserFilter() {
+            @Override
+            public boolean doFilter(org.apache.ibatis.reflection.MetaObject metaObject) {
+                Object statementId = metaObject.getValue("delegate.mappedStatement.id");
+                if (statementId == null) {
+                    return false;
+                }
+                boolean exclude = tenantProperties.shouldExcludeStatement(statementId.toString());
+                if (exclude) {
+                    log.debug("Skip tenant SQL parser for statement: {}", statementId);
+                }
+                return exclude;
+            }
+        });
         return interceptor;
     }
 //
@@ -69,7 +91,8 @@ public class MybatisPlusConfig {
 
     @Primary
     @Bean
-    public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dataSource, ApplicationContext appContext, PaginationInterceptor paginationInterceptor) throws Exception {
+    public MybatisSqlSessionFactoryBean sqlSessionFactory(DataSource dataSource, ApplicationContext appContext,
+            PaginationInterceptor paginationInterceptor, TenantProperties tenantProperties) throws Exception {
 
         log.error("sqlSessionFactory--先走");
 
@@ -90,7 +113,7 @@ public class MybatisPlusConfig {
 //        mybatisPlus.setTypeAliasesPackage("com.storlead..modules.*.pojo");
 //        mybatisPlus.setTypeEnumsPackage("com.storlead.sa.modules.console.enums,com.storlead.tems.modules.perform.enums");
         GlobalConfig globalConfig = new GlobalConfig();
-        globalConfig.setMetaObjectHandler(new MyMetaObjectHandler());
+        globalConfig.setMetaObjectHandler(new MyMetaObjectHandler(tenantProperties));
         mybatisPlus.setGlobalConfig(globalConfig);
 
         return mybatisPlus;

+ 17 - 0
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/service/MyBaseService.java

@@ -42,14 +42,31 @@ public interface MyBaseService <T> extends IService<T> {
 
     Boolean lgDelete(Long id);
 
+    Boolean lgCustomerDelete(Long customerId);
+
+    Boolean lgCustomerListDelete(List<Long> customerIdList);
+
     Boolean lgDelete(List<Long> ids);
 
     List<T> listByIds(List<Long> ids);
 
+    Boolean changeCustomerId(Long fromId, Long customerId);
+
     T getNextById(Serializable currentId, LambdaQueryWrapper<T> queryWrapper);
 
     T getPreviousById(Serializable currentId, LambdaQueryWrapper<T> queryWrapper);
 
+//    T getOldEntityById(Class<T> clazz, Long id);
+
     Class<T> getEntityClass();
 
+    boolean changeResourceTypeId(String resourceType, Long fromId, Long id);
+
+    boolean deleteByResourceId(String resourceType, Long resourceTypeId);
+
+    boolean deleteByResourceIds(String resourceType, List<Long> resourceTypeIds);
+
+    List<T> listByResourceId(String resourceType, Long resourceTypeId);
+
+    List<T> listByResourceIds(String resourceType, List<Long> resourceTypeIds);
 }

+ 105 - 18
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/service/impl/MyBaseServiceImpl.java

@@ -131,6 +131,30 @@ public class MyBaseServiceImpl<M extends MyBaseMapper<T>, T extends SysBaseField
         return this.update(updateWrapper);
     }
 
+    @Override
+    public Boolean lgCustomerDelete(Long customerId) {
+        if (Objects.isNull(customerId)) {
+            return false;
+        }
+        UpdateWrapper updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("customer_id",customerId);
+        updateWrapper.set("is_delete",CommonConstant.DEL_FLAG_1);
+        updateWrapper.set("update_time", DateUtils.now());
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public Boolean lgCustomerListDelete(List<Long> customerIdList) {
+        if (CollectionUtils.isEmpty(customerIdList)) {
+            return false;
+        }
+        UpdateWrapper updateWrapper = new UpdateWrapper<>();
+        updateWrapper.in("customer_id",customerIdList);
+        updateWrapper.set("is_delete",CommonConstant.DEL_FLAG_1);
+        updateWrapper.set("update_time",DateUtils.now());
+        return this.update(updateWrapper);
+    }
+
     @Override
     public Boolean lgDelete(List<Long> ids) {
         if (CollectionUtils.isEmpty(ids)) {
@@ -152,14 +176,77 @@ public class MyBaseServiceImpl<M extends MyBaseMapper<T>, T extends SysBaseField
         return this.list(query);
     }
 
+    @Override
+    public Boolean changeCustomerId(Long fromId, Long customerId) {
+        if (Objects.isNull(customerId) || Objects.isNull(fromId)) {
+            return false;
+        }
+        UpdateWrapper updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("customer_id",fromId);
+        updateWrapper.set("customer_id",customerId);
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public boolean changeResourceTypeId(String resourceType, Long fromId, Long id) {
+        if (Objects.isNull(id) || Objects.isNull(fromId)) {
+            return false;
+        }
+        UpdateWrapper updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("resource_type",resourceType);
+        updateWrapper.eq("resource_type_id",fromId);
+        updateWrapper.set("resource_type_id",id);
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public boolean deleteByResourceId(String resourceType, Long resourceTypeId) {
+        if (Objects.isNull(resourceTypeId)) {
+            return false;
+        }
+        UpdateWrapper updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("resource_type",resourceType);
+        updateWrapper.eq("resource_type_id",resourceTypeId);
+        updateWrapper.set("is_delete",CommonConstant.DEL_FLAG_1);
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public boolean deleteByResourceIds(String resourceType, List<Long> resourceTypeIds) {
+        if (CollectionUtils.isEmpty(resourceTypeIds)) {
+            return false;
+        }
+        UpdateWrapper updateWrapper = new UpdateWrapper<>();
+        updateWrapper.eq("resource_type",resourceType);
+        updateWrapper.in("resource_type_id",resourceTypeIds);
+        updateWrapper.set("is_delete",CommonConstant.DEL_FLAG_1);
+        return this.update(updateWrapper);
+    }
+
+    @Override
+    public List<T> listByResourceId(String resourceType, Long resourceTypeId) {
+        QueryWrapper wrapper = new QueryWrapper();
+        wrapper.eq("resource_type",resourceType);
+        wrapper.eq("resource_type_id",resourceTypeId);
+        return this.baseMapper.selectList(wrapper);
+    }
+
+    @Override
+    public List<T> listByResourceIds(String resourceType, List<Long> resourceTypeIds) {
+        QueryWrapper wrapper = new QueryWrapper();
+        wrapper.eq("resource_type",resourceType);
+        wrapper.in("resource_type_id",resourceTypeIds);
+        return this.baseMapper.selectList(wrapper);
+    }
+
     private <E extends QueryBaseEntity> void assemblyDataSource(LambdaQueryWrapper<T> wrapper,E e,String mergeSql) {
         try {
             log.error("进入assemblyDataSource---LambdaQueryWrapper");
-//            Object myClassInstance = SpringContextUtils.getBean("dataScope");
-//            Class<?> clazz = myClassInstance.getClass();
-//            Method method = clazz.getMethod("assemblyDataWrapperPurview", LambdaQueryWrapper.class,QueryBaseEntity.class,String.class);
-//            // 调用未编写的方法
-//            method.invoke(myClassInstance,wrapper,e,mergeSql);
+            Object myClassInstance = SpringContextUtils.getBean("dataScope");
+            Class<?> clazz = myClassInstance.getClass();
+            Method method = clazz.getMethod("assemblyDataWrapperPurview", LambdaQueryWrapper.class,QueryBaseEntity.class,String.class);
+            // 调用未编写的方法
+            method.invoke(myClassInstance,wrapper,e,mergeSql);
         } catch (Exception e1) {
             log.error("方法不存在",e1);
         }
@@ -168,11 +255,11 @@ public class MyBaseServiceImpl<M extends MyBaseMapper<T>, T extends SysBaseField
     private <E extends QueryBaseEntity> void assemblyDataOnlySource(LambdaQueryWrapper<T> wrapper,String mergeSql) {
         try {
             log.error("进入assemblyDataSource---LambdaQueryWrapper");
-//            Object myClassInstance = SpringContextUtils.getBean("dataScope");
-//            Class<?> clazz = myClassInstance.getClass();
-//            Method method = clazz.getMethod("assemblyDataOnlyWrapper", LambdaQueryWrapper.class,String.class);
-//            // 调用未编写的方法
-//            method.invoke(myClassInstance,wrapper,mergeSql);
+            Object myClassInstance = SpringContextUtils.getBean("dataScope");
+            Class<?> clazz = myClassInstance.getClass();
+            Method method = clazz.getMethod("assemblyDataOnlyWrapper", LambdaQueryWrapper.class,String.class);
+            // 调用未编写的方法
+            method.invoke(myClassInstance,wrapper,mergeSql);
         } catch (Exception e1) {
             log.error("方法不存在",e1);
         }
@@ -182,14 +269,14 @@ public class MyBaseServiceImpl<M extends MyBaseMapper<T>, T extends SysBaseField
         try {
             log.error("assemblyDataSource---QueryBaseEntity");
 
-//            // 假设我们有一个类名为"MyClass"但没有编写该类的实例
-//            Class<?> clazz = Class.forName("DataScope");
-//            // 使用反射创建一个实例
-//            Object myClassInstance = clazz.newInstance();
-//            // 使用反射获取方法
-//            Method method = clazz.getMethod("assemblyDataEntityPurview", QueryBaseEntity.class);
-//            // 调用未编写的方法
-//            method.invoke(myClassInstance, e);
+            // 假设我们有一个类名为"MyClass"但没有编写该类的实例
+            Class<?> clazz = Class.forName("DataScope");
+            // 使用反射创建一个实例
+            Object myClassInstance = clazz.newInstance();
+            // 使用反射获取方法
+            Method method = clazz.getMethod("assemblyDataEntityPurview", QueryBaseEntity.class);
+            // 调用未编写的方法
+            method.invoke(myClassInstance, e);
         } catch (Exception e1) {
             log.error("方法不存在",e1);
         }

+ 4 - 16
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/tenant/StorleadTenantHandler.java

@@ -5,10 +5,11 @@ import com.storlead.framework.core.tenant.TenantContext;
 import net.sf.jsqlparser.expression.Expression;
 import net.sf.jsqlparser.expression.LongValue;
 
-import java.util.Locale;
-
 /**
  * MyBatis-Plus 3.1.x {@link com.baomidou.mybatisplus.extension.plugins.tenant.TenantSqlParser} 使用的租户解析器。
+ * <p>
+ * {@link TenantHandler#doTableFilter(String)} 返回 true 表示跳过租户条件;仅白名单表返回 false。
+ * </p>
  */
 public class StorleadTenantHandler implements TenantHandler {
 
@@ -37,19 +38,6 @@ public class StorleadTenantHandler implements TenantHandler {
 
     @Override
     public boolean doTableFilter(String tableName) {
-        if (tableName == null) {
-            return true;
-        }
-        String normalized = tableName.replace("`", "").toLowerCase(Locale.ROOT);
-        for (String raw : properties.getIgnoreTables()) {
-            if (raw == null) {
-                continue;
-            }
-            String t = raw.replace("`", "").toLowerCase(Locale.ROOT);
-            if (t.equals(normalized)) {
-                return true;
-            }
-        }
-        return false;
+        return !properties.shouldApplyTenantLine(tableName);
     }
 }

+ 77 - 2
storlead-framework/storlead-mybatis/src/main/java/com/storlead/framework/mybatis/tenant/TenantProperties.java

@@ -5,21 +5,96 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 
 /**
- * 多租户行拦截配置:忽略无 tenant_id 列的表等。
+ * 多租户行拦截配置。
+ * <p>
+ * 默认不对任何表注入 {@code tenant_id};仅 {@link #includeTables} 中配置的表在 {@link #enabled}=true 时参与租户隔离。
+ * </p>
  */
 @Data
 @ConfigurationProperties(prefix = "storlead.tenant")
 public class TenantProperties {
 
+    /**
+     * 是否开启多租户 SQL 行拦截与插入填充(false 时所有表均不处理 tenant_id)。
+     */
+    private boolean enabled = false;
+
     /**
      * 数据库列名,需与实体字段映射一致(默认 tenant_id)。
      */
     private String columnName = "tenant_id";
 
     /**
-     * 不参与租户条件注入的表名(小写,不含库名)
+     * 需要租户隔离的表白名单(小写表名,不含库名)。未列入的表不会拼接 tenant_id 条件
      */
+    private List<String> includeTables = new ArrayList<>();
+
+    /**
+     * @deprecated 已改为 {@link #includeTables} 白名单模式;仅当 includeTables 为空且本列表非空时,仍按旧黑名单逻辑兼容。
+     */
+    @Deprecated
     private List<String> ignoreTables = new ArrayList<>();
+
+    /**
+     * 不参与租户 SQL 解析的 Mapper 语句(全限定 statementId,或后缀匹配如 HrmresourceMapper.updatePermanentDate)。
+     * 多表 JOIN 的 UPDATE/复杂 SELECT 无法被 TenantSqlParser 处理,须排除。
+     */
+    private List<String> excludeStatementIds = defaultExcludeStatementIds();
+
+    private static List<String> defaultExcludeStatementIds() {
+        List<String> ids = new ArrayList<>();
+        ids.add("com.storlead.user.mapper.HrmresourceMapper.updatePermanentDate");
+        ids.add("com.storlead.user.mapper.HrmresourceMapper.updateResignDate");
+        ids.add("com.storlead.user.mapper.HrmresourceMapper.getUserList");
+        return ids;
+    }
+
+    /**
+     * 当前表是否应注入 / 填充 tenant_id。
+     */
+    public boolean shouldApplyTenantLine(String tableName) {
+        if (!enabled || tableName == null) {
+            return false;
+        }
+        String normalized = normalizeTableName(tableName);
+        if (!includeTables.isEmpty()) {
+            return includeTables.stream().anyMatch(t -> tableNameEquals(normalized, t));
+        }
+        if (!ignoreTables.isEmpty()) {
+            return ignoreTables.stream().noneMatch(t -> tableNameEquals(normalized, t));
+        }
+        return false;
+    }
+
+    /**
+     * 是否跳过 MyBatis 租户 SQL 解析(复杂 JOIN / 多表 UPDATE 等)。
+     */
+    public boolean shouldExcludeStatement(String statementId) {
+        if (statementId == null || excludeStatementIds == null || excludeStatementIds.isEmpty()) {
+            return false;
+        }
+        for (String pattern : excludeStatementIds) {
+            if (pattern == null || pattern.isEmpty()) {
+                continue;
+            }
+            if (statementId.equals(pattern) || statementId.endsWith("." + pattern)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public static String normalizeTableName(String tableName) {
+        return tableName.replace("`", "").toLowerCase(Locale.ROOT);
+    }
+
+    private static boolean tableNameEquals(String normalized, String configured) {
+        if (configured == null) {
+            return false;
+        }
+        return normalized.equals(normalizeTableName(configured));
+    }
 }

+ 89 - 68
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/config/Swagger2Config.java

@@ -1,7 +1,9 @@
 package com.storlead.framework.web.config;
 
+import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
 import com.storlead.framework.common.constant.DefContants;
-import io.swagger.annotations.ApiOperation;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
 import org.springframework.boot.actuate.autoconfigure.endpoint.web.CorsEndpointProperties;
 import org.springframework.boot.actuate.autoconfigure.endpoint.web.WebEndpointProperties;
 import org.springframework.boot.actuate.autoconfigure.web.server.ManagementPortType;
@@ -13,53 +15,90 @@ import org.springframework.boot.actuate.endpoint.web.servlet.WebMvcEndpointHandl
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.core.env.Environment;
+import org.springframework.util.ReflectionUtils;
 import org.springframework.util.StringUtils;
 import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
 import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping;
 import springfox.documentation.builders.ApiInfoBuilder;
-import springfox.documentation.builders.ParameterBuilder;
 import springfox.documentation.builders.PathSelectors;
 import springfox.documentation.builders.RequestHandlerSelectors;
-import springfox.documentation.schema.ModelRef;
 import springfox.documentation.service.*;
 import springfox.documentation.spi.DocumentationType;
 import springfox.documentation.spi.service.contexts.SecurityContext;
 import springfox.documentation.spring.web.plugins.Docket;
+import springfox.documentation.swagger2.annotations.EnableSwagger2;
+import springfox.documentation.spring.web.plugins.WebFluxRequestHandlerProvider;
+import springfox.documentation.spring.web.plugins.WebMvcRequestHandlerProvider;
 
+import java.lang.reflect.Field;
 import java.util.*;
+import java.util.stream.Collectors;
 
 /**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2024-03-29 17:35
+ * Springfox 3 + Knife4j 3:统一扫描 com.storlead 下全部 Controller。
  */
-
 @Configuration
+@EnableSwagger2
+@EnableKnife4j
 public class Swagger2Config implements WebMvcConfigurer {
 
-    // 显示swagger-ui.html文档展示页,还必须注入swagger资源:
-
     @Override
     public void addResourceHandlers(ResourceHandlerRegistry registry) {
-        registry.addResourceHandler("swagger-ui.html").addResourceLocations("classpath:/META-INF/resources/");
-        registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
-        registry.addResourceHandler("/webjars/").addResourceLocations("classpath:/META-INF/resources/webjars/");
+        registry.addResourceHandler("/swagger-ui.html")
+                .addResourceLocations("classpath:/META-INF/resources/");
+        registry.addResourceHandler("/doc.html")
+                .addResourceLocations("classpath:/META-INF/resources/");
+        registry.addResourceHandler("/webjars/**")
+                .addResourceLocations("classpath:/META-INF/resources/webjars/");
     }
 
-    //
-    // swagger2的配置文件,这里可以配置swagger2的一些基本的内容,比如扫描的包等等
-
-    // @return Docket
+    /**
+     * Spring Boot 2.6+ 与 Springfox 兼容:去掉使用 PathPatternParser 的 HandlerMapping。
+     */
+    @Bean
+    public static BeanPostProcessor springfoxHandlerProviderBeanPostProcessor() {
+        return new BeanPostProcessor() {
+            @Override
+            public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
+                if (bean instanceof WebMvcRequestHandlerProvider || bean instanceof WebFluxRequestHandlerProvider) {
+                    customizeSpringfoxHandlerMappings(getHandlerMappings(bean));
+                }
+                return bean;
+            }
+
+            private <T extends RequestMappingInfoHandlerMapping> void customizeSpringfoxHandlerMappings(List<T> mappings) {
+                List<T> copy = mappings.stream()
+                        .filter(mapping -> mapping.getPatternParser() == null)
+                        .collect(Collectors.toList());
+                mappings.clear();
+                mappings.addAll(copy);
+            }
+
+            @SuppressWarnings("unchecked")
+            private List<RequestMappingInfoHandlerMapping> getHandlerMappings(Object bean) {
+                Field field = ReflectionUtils.findField(bean.getClass(), "handlerMappings");
+                if (field == null) {
+                    return Collections.emptyList();
+                }
+                ReflectionUtils.makeAccessible(field);
+                try {
+                    return (List<RequestMappingInfoHandlerMapping>) field.get(bean);
+                } catch (IllegalAccessException e) {
+                    throw new IllegalStateException(e);
+                }
+            }
+        };
+    }
 
     @Bean
-    public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier
-            , ServletEndpointsSupplier servletEndpointsSupplier
-            , ControllerEndpointsSupplier controllerEndpointsSupplier
-            , EndpointMediaTypes endpointMediaTypes
-            , CorsEndpointProperties corsProperties
-            , WebEndpointProperties webEndpointProperties
-            , Environment environment) {
+    public WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping(WebEndpointsSupplier webEndpointsSupplier,
+            ServletEndpointsSupplier servletEndpointsSupplier,
+            ControllerEndpointsSupplier controllerEndpointsSupplier,
+            EndpointMediaTypes endpointMediaTypes,
+            CorsEndpointProperties corsProperties,
+            WebEndpointProperties webEndpointProperties,
+            Environment environment) {
         List<ExposableEndpoint<?>> allEndpoints = new ArrayList<>();
         Collection<ExposableWebEndpoint> webEndpoints = webEndpointsSupplier.getEndpoints();
         allEndpoints.addAll(webEndpoints);
@@ -67,72 +106,54 @@ public class Swagger2Config implements WebMvcConfigurer {
         allEndpoints.addAll(controllerEndpointsSupplier.getEndpoints());
         String basePath = webEndpointProperties.getBasePath();
         EndpointMapping endpointMapping = new EndpointMapping(basePath);
-        boolean shouldRegisterLinksMapping = this.shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
-        return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes, corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath), shouldRegisterLinksMapping, null);
-    }
-    private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment, String basePath) {
-        return webEndpointProperties.getDiscovery().isEnabled() && (StringUtils.hasText(basePath) || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
+        boolean shouldRegisterLinksMapping = shouldRegisterLinksMapping(webEndpointProperties, environment, basePath);
+        return new WebMvcEndpointHandlerMapping(endpointMapping, webEndpoints, endpointMediaTypes,
+                corsProperties.toCorsConfiguration(), new EndpointLinksResolver(allEndpoints, basePath),
+                shouldRegisterLinksMapping, null);
     }
 
-    @Bean
-    public Docket platformAdminApi() {
-        return buildGroupedDocket("platform-admin", "/platform/admin/.*");
+    private boolean shouldRegisterLinksMapping(WebEndpointProperties webEndpointProperties, Environment environment,
+            String basePath) {
+        return webEndpointProperties.getDiscovery().isEnabled()
+                && (StringUtils.hasText(basePath)
+                || ManagementPortType.get(environment).equals(ManagementPortType.DIFFERENT));
     }
 
+    /**
+     * 单一分组,扫描 com.storlead 包下全部接口(不限制 @ApiOperation,避免漏接口)。
+     */
     @Bean
-    public Docket tenantAccountApi() {
-        return buildGroupedDocket("tenant-account", "/account/.*");
-    }
-
-    private Docket buildGroupedDocket(String groupName, String pathRegex) {
+    public Docket allApiDocket() {
         return new Docket(DocumentationType.SWAGGER_2)
-                .groupName(groupName)
+                .groupName("all")
                 .apiInfo(apiInfo())
                 .select()
                 .apis(RequestHandlerSelectors.basePackage("com.storlead"))
-                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
-                .paths(PathSelectors.regex(pathRegex))
+                .paths(PathSelectors.any())
                 .build()
-                .securityContexts(Arrays.asList(securityContext()))
-                .securitySchemes(Arrays.asList(new ApiKey("token", "token", io.swagger.v3.oas.models.security.SecurityScheme.In.HEADER.name())));
+                .securityContexts(Collections.singletonList(securityContext()))
+                .securitySchemes(Collections.singletonList(new ApiKey("token", DefContants.ACCESS_TOKEN, "header")));
     }
 
     private SecurityContext securityContext() {
         return SecurityContext.builder()
                 .securityReferences(defaultAuth())
-                //.forPaths(PathSelectors.regex("/*.*"))
                 .build();
     }
 
     private List<SecurityReference> defaultAuth() {
-        AuthorizationScope authorizationScope
-                = new AuthorizationScope("global", "accessEverything");
-        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
-        authorizationScopes[0] = authorizationScope;
-        return Collections.singletonList(
-                new SecurityReference("token", authorizationScopes));
-    }
-
-    @Bean
-    SecurityScheme securityScheme() {
-        return new ApiKey(DefContants.ACCESS_TOKEN, DefContants.ACCESS_TOKEN, "header");
-    }
-
-    private List<Parameter> setHeaderToken() {
-        ParameterBuilder tokenPar = new ParameterBuilder();
-        List<Parameter> pars = new ArrayList<>();
-        tokenPar.name("token").description("token").modelRef(new ModelRef("string")).parameterType("header")
-                .required(false).build();
-        pars.add(tokenPar.build());
-        return pars;
+        AuthorizationScope scope = new AuthorizationScope("global", "accessEverything");
+        return Collections.singletonList(new SecurityReference("token", new AuthorizationScope[]{scope}));
     }
 
     private ApiInfo apiInfo() {
-        return new ApiInfoBuilder() // //大标题
-                .title("领存CRM后台服务API接口文档") // 版本号 .version("1.0") //
-                .termsOfServiceUrl("NO terms of service") // 描述 .description("后台API接口")
+        return new ApiInfoBuilder()
+                .title("领存CRM后台服务API接口文档")
+                .description("com.storlead 包下全部 REST 接口")
+                .version("1.0")
+                .termsOfServiceUrl("NO terms of service")
                 .license("The Apache License, Version 2.0")
-                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html").build();
+                .licenseUrl("http://www.apache.org/licenses/LICENSE-2.0.html")
+                .build();
     }
-
 }

+ 57 - 57
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/filter/AuthRequestFilter.java

@@ -25,6 +25,7 @@ import com.storlead.framework.web.wrapper.JsonRequestWrapper;
 import lombok.extern.log4j.Log4j2;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.core.Ordered;
+import org.springframework.dao.DataAccessException;
 import org.springframework.http.HttpStatus;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
@@ -139,72 +140,71 @@ public class AuthRequestFilter implements Filter, Ordered {
         }
 
         if (StringUtils.isNotBlank(token)) {
-            String jsonInfo = redisService.getCacheObject(token);
-            if (StrUtil.isNotBlank(jsonInfo) && jsonInfo.length() < "1efb52db4da5d2b92516a4eba0b3145b34234234".length()) {
-                jsonInfo = redisService.getCacheObject(jsonInfo);
-            }
-            if (StringUtils.isNotBlank(jsonInfo)) {
-                try {
-                    IContext context = Context.getContext();
-                    LoginUser loginUserInfo = JSONUtil.toBean(jsonInfo, LoginUser.class);
+            try {
+                String jsonInfo = redisService.getCacheObject(token);
+                if (StrUtil.isNotBlank(jsonInfo) && jsonInfo.length() < "1efb52db4da5d2b92516a4eba0b3145b34234234".length()) {
+                    jsonInfo = redisService.getCacheObject(jsonInfo);
+                }
+                if (StringUtils.isBlank(jsonInfo)) {
+                    return false;
+                }
+                IContext context = Context.getContext();
+                LoginUser loginUserInfo = JSONUtil.toBean(jsonInfo, LoginUser.class);
 
-                    String  scopeMenuId =  context.getAttribute("scopeMenuId",String.class);
-                    Object  commonScope =  context.getAttribute("commonScope",Object.class);
-                    Object  commonScopeType =  context.getAttribute("commonScopeType",Object.class);
-                    // 当前接口的地址
-                    loginUserInfo.setApiUrl(url);
-                    loginUserInfo.setScopeMenuId(scopeMenuId);
-                    if (Objects.nonNull(commonScope)) {
-                        loginUserInfo.setCommonScope((Integer)commonScope);
-                    }
-                    if (Objects.nonNull(commonScopeType)) {
-                        loginUserInfo.setCommonScopeType(commonScopeType.toString());
-                    }
-                    context.setAttribute(UserCacheKeyConstants.LOGIN_USER_INFO_KEY, loginUserInfo);
-                    context.setAttribute(UserCacheKeyConstants.LOGIN_USER_INFO_TOKEN_KEY, token);
-                    context.setAttribute(UserCacheKeyConstants.LOGIN_USER_INFO_ID_KEY, loginUserInfo.getId());
+                String scopeMenuId = context.getAttribute("scopeMenuId", String.class);
+                Object commonScope = context.getAttribute("commonScope", Object.class);
+                Object commonScopeType = context.getAttribute("commonScopeType", Object.class);
+                loginUserInfo.setApiUrl(url);
+                loginUserInfo.setScopeMenuId(scopeMenuId);
+                if (Objects.nonNull(commonScope)) {
+                    loginUserInfo.setCommonScope((Integer) commonScope);
+                }
+                if (Objects.nonNull(commonScopeType)) {
+                    loginUserInfo.setCommonScopeType(commonScopeType.toString());
+                }
+                context.setAttribute(UserCacheKeyConstants.LOGIN_USER_INFO_KEY, loginUserInfo);
+                context.setAttribute(UserCacheKeyConstants.LOGIN_USER_INFO_TOKEN_KEY, token);
+                context.setAttribute(UserCacheKeyConstants.LOGIN_USER_INFO_ID_KEY, loginUserInfo.getId());
 
-                    Long tenantId = loginUserInfo.getTenantId();
-                    if (tenantId == null) {
-                        tenantId = loginUserInfo.getCompanyId();
-                    }
-                    if (tenantId != null) {
-                        TenantContext.bindTenantId(tenantId);
-                    }
-                    String headerTenant = req.getHeader(TenantConstants.HTTP_HEADER_TENANT_ID);
-                    if (StringUtils.isNotBlank(headerTenant)) {
-                        try {
-                            Long headerTid = Long.valueOf(headerTenant.trim());
-                            if (tenantId != null && !tenantId.equals(headerTid)) {
-                                log.warn("X-Tenant-Id {} 与登录租户 {} 不一致,已以登录信息为准", headerTid, tenantId);
-                            } else if (tenantId == null && Boolean.TRUE.equals(loginUserInfo.getIsAdmin())) {
-                                TenantContext.bindTenantId(headerTid);
-                            }
-                        } catch (NumberFormatException e) {
-                            log.warn("非法 X-Tenant-Id 请求头: {}", headerTenant);
+                Long tenantId = loginUserInfo.getTenantId();
+                if (tenantId == null) {
+                    tenantId = loginUserInfo.getCompanyId();
+                }
+                if (tenantId != null) {
+                    TenantContext.bindTenantId(tenantId);
+                }
+                String headerTenant = req.getHeader(TenantConstants.HTTP_HEADER_TENANT_ID);
+                if (StringUtils.isNotBlank(headerTenant)) {
+                    try {
+                        Long headerTid = Long.valueOf(headerTenant.trim());
+                        if (tenantId != null && !tenantId.equals(headerTid)) {
+                            log.warn("X-Tenant-Id {} 与登录租户 {} 不一致,已以登录信息为准", headerTid, tenantId);
+                        } else if (tenantId == null && Boolean.TRUE.equals(loginUserInfo.getIsAdmin())) {
+                            TenantContext.bindTenantId(headerTid);
                         }
+                    } catch (NumberFormatException e) {
+                        log.warn("非法 X-Tenant-Id 请求头: {}", headerTenant);
                     }
-                    if (TenantContext.getTenantId() == null) {
-                        log.warn("已登录用户未解析到租户 ID(LoginUser.tenantId / companyId 均为空),多租户条件将不生效: userId={}", loginUserInfo.getId());
-                    }
+                }
+                if (TenantContext.getTenantId() == null) {
+                    log.warn("已登录用户未解析到租户 ID(LoginUser.tenantId / companyId 均为空),多租户条件将不生效: userId={}", loginUserInfo.getId());
+                }
 
-                    // 修改失效时间,超过24小时没有使用,
-                    Long expire = redisService.getCacheExpire(token,TimeUnit.SECONDS);
-                    log.debug("getCacheExpire-----------当前用户信息 = "+loginUserInfo);
-                    if (expire < 3600) {
-                        redisService.setCacheExpire(token,60 * 60 * 24L * 7, TimeUnit.SECONDS);
-                    }
-                } catch (Exception e) {
-                    log.error("loginHandle error",e);
-                    return false;
+                Long expire = redisService.getCacheExpire(token, TimeUnit.SECONDS);
+                log.debug("getCacheExpire-----------当前用户信息 = " + loginUserInfo);
+                if (expire != null && expire < 3600) {
+                    redisService.setCacheExpire(token, 60 * 60 * 24L * 7, TimeUnit.SECONDS);
                 }
-            } else {
+                return true;
+            } catch (DataAccessException e) {
+                log.error("loginHandle redis unavailable, url={}", url, e);
+                return false;
+            } catch (Exception e) {
+                log.error("loginHandle error, url={}", url, e);
                 return false;
             }
-        } else {
-            return false;
         }
-        return true;
+        return false;
     }
 
     @Override

+ 1 - 1
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/interceptor/WebConfig.java

@@ -1,4 +1,4 @@
-//package com.storlead.framework.web.interceptor;
+package com.storlead.framework.web.interceptor;//package com.storlead.framework.web.interceptor;
 //
 //import com.storlead.utils.common.FileProperties;
 //import org.springframework.beans.factory.annotation.Autowired;

+ 1 - 1
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/message/WebResp.java

@@ -1,4 +1,4 @@
-//package com.storlead.framework.web.message;
+package com.storlead.framework.web.message;//package com.storlead.framework.web.message;
 //
 //import lombok.Data;
 //import lombok.NoArgsConstructor;

+ 45 - 12
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/process/ApiResultHandler.java

@@ -1,12 +1,15 @@
-//package com.storlead.framework.web.process;
+package com.storlead.framework.web.process;//package com.storlead.framework.web.process;
 //
 //import com.fasterxml.jackson.core.JsonParser;
 //import com.fasterxml.jackson.databind.DeserializationFeature;
 //import com.fasterxml.jackson.databind.ObjectMapper;
-//import com.storlead.framework.web.message.WebResp;
+//import com.alibaba.fastjson.JSON;
+//import com.alibaba.fastjson.JSONObject;
+//import com.storlead.framework.common.result.Result;
 //import org.apache.commons.lang3.StringUtils;
 //import org.springframework.core.MethodParameter;
 //import org.springframework.http.MediaType;
+//import org.springframework.http.ResponseEntity;
 //import org.springframework.http.converter.HttpMessageConverter;
 //import org.springframework.http.server.ServerHttpRequest;
 //import org.springframework.http.server.ServerHttpResponse;
@@ -36,6 +39,10 @@
 //
 //    @Override
 //    public boolean supports(MethodParameter returnType, Class converterType) {
+//        // 不处理 ResponseEntity 返回值,避免图片/文件流等被包装导致 ClassCastException
+//        if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
+//            return false;
+//        }
 //        AnnotatedElement element = returnType.getAnnotatedElement();
 //        return Arrays.stream(annos).anyMatch(anno -> anno.isAnnotation() && element.isAnnotationPresent(anno));
 //    }
@@ -64,16 +71,15 @@
 //    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
 //                                  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
 //                                  ServerHttpResponse response) {
+//        Object data = normalizeResult(body);
+//        data = wrapIfNeed(data, returnType);
+//        data = normalizeResult(data);
 //
-//        if (MediaType.TEXT_EVENT_STREAM.includes(selectedContentType)) {
-//            return body;
-//        }
 //
-//        Object data = wrapIfNeed(body, returnType);
 //        String selectedConverterTypeClassName = selectedConverterType.getName();
 //        // selectedConverterType 对象是  StringHttpMessageConverter时, 表示不能直接返回  WebResp, 要不然会报错      org.springframework.http.converter.StringHttpMessageConverter
 //        if (selectedConverterTypeClassName.indexOf("StringHttpMessageConverter") >= 0) {    // 如果是返回  string 类型的, 就返回 string 类型的
-//            if (data instanceof WebResp) {
+//            if (data instanceof Result) {
 //                ObjectMapper mapper = new ObjectMapper();
 //                mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
 //                mapper.enable(JsonParser.Feature.ALLOW_UNQUOTED_CONTROL_CHARS);
@@ -93,16 +99,43 @@
 //    }
 //
 //    public static Object wrapIfNeed(Object body, MethodParameter returnType) {
-//        if (body instanceof WebResp) {
+//        // 对于已经是统一返回类型的结果或资源类,直接透传,避免二次包装导致类型转换异常
+//        if (body instanceof Result) {
+//            return normalizeResult(body);
+//        } else if (body instanceof org.springframework.core.io.Resource) {
+//            // 文件预览/下载等场景需要直接返回二进制流
 //            return body;
-//        } else {
+//        } else if (body instanceof org.springframework.http.ResponseEntity) {
+//            // ResponseEntity 如 404、图片预览等,直接透传,避免包装导致序列化异常
+//            org.springframework.http.ResponseEntity<?> resp = (org.springframework.http.ResponseEntity<?>) body;
+//            Object respBody = resp.getBody();
+//            if (respBody == null || respBody instanceof org.springframework.core.io.Resource) {
+//                return body;
+//            }
+//        }
+//        {
 //            if (body != null) {
-//                return new WebResp(body);
+//                return Result.result(body);
 //            } else if(body == null) {
-//                return new WebResp("");
+//                return Result.ok();
 //            }
-//            return new WebResp(getDefaultValue(returnType));
+//            return Result.result(getDefaultValue(returnType));
+//        }
+//    }
+//
+//    /**
+//     * fastjson.JSONObject 若被 Jackson 当作 Bean 序列化会产生乱码;统一转为普通 Map。
+//     */
+//    private static Object normalizeResult(Object body) {
+//        if (!(body instanceof Result)) {
+//            return body;
+//        }
+//        Result<?> result = (Result<?>) body;
+//        Object payload = result.getResult();
+//        if (payload instanceof JSONObject) {
+//            result.setResult(JSON.parseObject(JSON.toJSONString(payload)));
 //        }
+//        return result;
 //    }
 //
 //    public static Object getDefaultValue(MethodParameter returnType) {

+ 26 - 0
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/process/StorleadFastJsonHttpMessageConverter.java

@@ -0,0 +1,26 @@
+package com.storlead.framework.web.process;
+
+import org.springframework.http.HttpOutputMessage;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.HttpMessageNotWritableException;
+import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
+
+import java.io.IOException;
+
+/**
+ * 保证 API 始终输出 UTF-8 文本 JSON。
+ */
+public class StorleadFastJsonHttpMessageConverter extends FastJsonHttpMessageConverter {
+
+    @Override
+    protected void writeInternal(Object object, HttpOutputMessage outputMessage)
+            throws IOException, HttpMessageNotWritableException {
+        if (object instanceof byte[]) {
+            byte[] bytes = (byte[]) object;
+            outputMessage.getHeaders().setContentType(MediaType.APPLICATION_JSON);
+            outputMessage.getBody().write(bytes, 0, bytes.length);
+            return;
+        }
+        super.writeInternal(object, outputMessage);
+    }
+}

+ 52 - 72
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/process/WebMvcConfigurer.java

@@ -4,59 +4,48 @@ import com.alibaba.fastjson.serializer.SerializerFeature;
 import com.alibaba.fastjson.support.config.FastJsonConfig;
 import com.alibaba.fastjson.support.spring.FastJsonHttpMessageConverter;
 import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonFactory;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import org.springframework.http.MediaType;
 import org.springframework.http.converter.HttpMessageConverter;
 import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
 import org.springframework.util.AntPathMatcher;
-import org.springframework.web.bind.annotation.ControllerAdvice;
-import org.springframework.web.servlet.HandlerInterceptor;
-import org.springframework.web.servlet.config.annotation.*;
+import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.PathMatchConfigurer;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
 
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import java.nio.charset.StandardCharsets;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 import java.util.List;
 
 @Configuration
 public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
 
+    private static final MediaType JSON_UTF8 = new MediaType("application", "json", StandardCharsets.UTF_8);
+
     @Autowired
     private ObjectMapper objectMapper;
 
     @Bean
-    public WebMvcConfigurer corsConfigurer() {
-        return new WebMvcConfigurer() {
+    public org.springframework.web.servlet.config.annotation.WebMvcConfigurer corsConfigurer() {
+        return new org.springframework.web.servlet.config.annotation.WebMvcConfigurer() {
             @Override
             public void addCorsMappings(CorsRegistry registry) {
-                registry.addMapping("/**"); //对应的接口
+                registry.addMapping("/**");
             }
         };
     }
 
-    public WebMvcConfigurer() {
-
-    }
-
-    /**
-     * 配置路由匹配模式
-     *
-     * **/
     @Override
     public void configurePathMatch(PathMatchConfigurer configurer) {
-        /***
-         *
-         * setUseSuffixPatternMatch : 设置是否是后缀模式匹配,如“/user”是否匹配/user.*,默认真即匹配;
-         * setUseTrailingSlashMatch : 设置是否自动后缀路径模式匹配,如“/user”是否匹配“/user/”,默认真即匹配;
-         configurer.setUseSuffixPatternMatch(false)
-         .setUseTrailingSlashMatch(true);
-         **/
         configurer.setUseSuffixPatternMatch(false);
         configurer.setUseTrailingSlashMatch(true);
         configurer.setUseRegisteredSuffixPatternMatch(true);
@@ -65,75 +54,66 @@ public class WebMvcConfigurer extends WebMvcConfigurerAdapter {
         configurer.setPathMatcher(pathMatcher);
     }
 
-    @Override
-    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
-        super.configureMessageConverters(converters);
-        FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();
-        FastJsonConfig fastJsonConfig = new FastJsonConfig();
-        fastJsonConfig.setSerializerFeatures(
-                SerializerFeature.PrettyFormat
-        );
-        fastConverter.setFastJsonConfig(fastJsonConfig);
-
-        converters.add(fastConverter);
-    }
-
-
-    /** 自定义一个MappingJackson2HttpMessageConverter放在所有默认Converter的前面,使其拥有最高优先级 */
+    /**
+     * FastJson 必须优先于 Jackson:Controller 大量返回 Result + fastjson.JSONObject,
+     * 若 Jackson 先处理会按 Bean 序列化 JSONObject 内部字段,客户端看到二进制乱码(:success...)。
+     */
     @Override
     public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
-        ArrayList<HttpMessageConverter<?>> copy = new ArrayList<>(converters.size());
+        List<HttpMessageConverter<?>> others = new ArrayList<>();
         Iterator<HttpMessageConverter<?>> iterator = converters.iterator();
         while (iterator.hasNext()) {
             HttpMessageConverter<?> converter = iterator.next();
-            if (converter instanceof MappingJackson2HttpMessageConverter) {
+            if (converter instanceof MappingJackson2HttpMessageConverter
+                    || converter instanceof FastJsonHttpMessageConverter) {
                 iterator.remove();
             } else {
-                copy.add(converter);
+                others.add(converter);
             }
         }
-        //converters.clear();
+        converters.add(0, createFastJsonHttpMessageConverter());
+        converters.add(1, createMappingJackson2HttpMessageConverter());
+        converters.addAll(others);
+    }
 
-        converters.add(createMappingJackson2HttpMessageConverter());
-        converters.addAll(copy);
+    private StorleadFastJsonHttpMessageConverter createFastJsonHttpMessageConverter() {
+        StorleadFastJsonHttpMessageConverter fastConverter = new StorleadFastJsonHttpMessageConverter();
+        FastJsonConfig fastJsonConfig = new FastJsonConfig();
+        fastJsonConfig.setCharset(StandardCharsets.UTF_8);
+        fastJsonConfig.setDateFormat("yyyy-MM-dd HH:mm:ss");
+        fastJsonConfig.setSerializerFeatures(
+                SerializerFeature.DisableCircularReferenceDetect,
+                SerializerFeature.WriteMapNullValue
+        );
+        fastConverter.setFastJsonConfig(fastJsonConfig);
+        fastConverter.setDefaultCharset(StandardCharsets.UTF_8);
+        fastConverter.setSupportedMediaTypes(Collections.singletonList(JSON_UTF8));
+        return fastConverter;
     }
 
     private MappingJackson2HttpMessageConverter createMappingJackson2HttpMessageConverter() {
-        if (objectMapper == null) {
-            final ObjectMapper objectMapper = new ObjectMapper();
-            objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
-            objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
-            this.objectMapper = objectMapper;
+        ObjectMapper mapper = this.objectMapper;
+        if (mapper == null || !(mapper.getFactory() instanceof JsonFactory)) {
+            mapper = new ObjectMapper(new JsonFactory());
+            mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+            mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"));
+            this.objectMapper = mapper;
         }
-        return new MappingJackson2HttpMessageConverter(this.objectMapper);
+        MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter(mapper);
+        jacksonConverter.setDefaultCharset(StandardCharsets.UTF_8);
+        jacksonConverter.setSupportedMediaTypes(Collections.singletonList(JSON_UTF8));
+        return jacksonConverter;
     }
 
-    /**
-     * 配置返回的数据内容 , 如 json,html
-     * **/
     @Override
     public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
-        /* 是否通过请求Url的扩展名来决定media type */
-        /**/
-        configurer.favorPathExtension(true)
-                // 不检查Accept请求头,客户端传过来Accept要求返回的请求头,不做理会
-                .ignoreAcceptHeader(true).parameterName("mediaType")
-                // 请求以.html与 .jsp结尾的会被当成MediaType.TEXT_HTML
-                .mediaType("html", MediaType.TEXT_HTML).mediaType("jsp", MediaType.TEXT_HTML)
-                // 请求以.do与.json结尾的会被当成MediaType.APPLICATION_JSON
-                .mediaType("do", MediaType.APPLICATION_JSON_UTF8).mediaType("json", MediaType.APPLICATION_JSON_UTF8);
-
-        /**
-         configurer.mediaTypes(ImmutableMap.of("do", MediaType.APPLICATION_JSON_UTF8,"json",MediaType.APPLICATION_JSON_UTF8 ));
-         **/
-        // configurer.mediaTypes(ImmutableMap.of("do", MediaType.APPLICATION_JSON_UTF8));
+        configurer.favorPathExtension(false)
+                .favorParameter(false)
+                .ignoreAcceptHeader(false)
+                .defaultContentType(JSON_UTF8);
     }
 
-    /**
-     * 添加拦截器
-     * **/
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-
     }
 }

+ 55 - 0
storlead-framework/storlead-web/src/main/java/com/storlead/framework/web/properties/FileProperties.java

@@ -0,0 +1,55 @@
+package com.storlead.framework.web.properties;
+
+import lombok.Data;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * @program: sp-sales
+ * @description:
+ * @author: chenkq
+ * @create: 2022-08-22 14:53
+ */
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "file")
+public class FileProperties {
+
+    /** 文件大小限制 */
+    private Long maxSize;
+
+    /** 头像大小限制 */
+    private Long avatarMaxSize;
+
+    /** 0:本地,1:OSS */
+    private Integer mode = 0;
+
+    /** 环境 */
+    private String active = "";
+
+    private ElPath mac;
+
+    private String filePath;
+
+    private ElPath linux;
+
+    private ElPath windows;
+
+    public ElPath getPath(){
+        String os = System.getProperty("os.name");
+        if(os.toLowerCase().startsWith("win")) {
+            return windows;
+        } else if(os.toLowerCase().startsWith("mac")){
+            return mac;
+        }
+        return linux;
+    }
+
+    @Data
+    public static class ElPath{
+
+        private String path;
+
+        private String avatar;
+    }
+}

+ 18 - 31
storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/AutoSendDelayEmailTaskJob.java

@@ -11,11 +11,8 @@ import com.storlead.sales.mail.service.EmailsService;
 import com.storlead.sales.mail.service.SmtpPopSettingsService;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.quartz.DisallowConcurrentExecution;
-import org.quartz.Job;
-import org.quartz.JobExecutionContext;
-import org.quartz.JobExecutionException;
 import org.springframework.core.env.Environment;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 
@@ -25,17 +22,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-/**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2024-11-01 15:27
- */
 @Slf4j
 @Component
 @NoArgsConstructor
-@DisallowConcurrentExecution
-public class AutoSendDelayEmailTaskJob implements Job {
+public class AutoSendDelayEmailTaskJob {
+
     @Resource
     private EmailsService emailsService;
 
@@ -45,20 +36,19 @@ public class AutoSendDelayEmailTaskJob implements Job {
     @Resource
     private SmtpPopSettingsService smtpPopSettingsService;
 
-    @Override
-    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
+    @Scheduled(cron = "${storlead.mail.scheduler.delay-send-cron:0 * * * * ?}")
+    public void sendDelayEmailTask() {
         String active = environment.getProperty("spring.profiles.active");
-        if("dev".equals(active)) {
-            return ;
+        if ("dev".equals(active)) {
+            return;
         }
         log.info("定时发送邮件任务开始");
         LambdaQueryWrapper<EmailsEntity> wrapper = new LambdaQueryWrapper<>();
         LocalDateTime now = LocalDateTime.now();
-        LocalDateTime triggerTime = now;
-        wrapper.select(EmailsEntity::getId,EmailsEntity::getSmtpPopId,EmailsEntity::getOwnerBy);
-        wrapper.le(EmailsEntity::getDelaySendTime,triggerTime);
+        wrapper.select(EmailsEntity::getId, EmailsEntity::getSmtpPopId, EmailsEntity::getOwnerBy);
+        wrapper.le(EmailsEntity::getDelaySendTime, now);
         wrapper.eq(EmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
-        wrapper.eq(EmailsEntity::getStatus,-1);
+        wrapper.eq(EmailsEntity::getStatus, -1);
         wrapper.eq(EmailsEntity::getFolder, EmailBoxEnum.SENTING.code);
         wrapper.isNotNull(EmailsEntity::getDelaySendTime);
         List<EmailsEntity> emailsls = emailsService.list(wrapper);
@@ -66,28 +56,25 @@ public class AutoSendDelayEmailTaskJob implements Job {
             return;
         }
         LambdaUpdateWrapper<EmailsEntity> upEntity = new LambdaUpdateWrapper<>();
-        upEntity.set(EmailsEntity::getStatus,0);
-        upEntity.in(EmailsEntity::getId,emailsls.stream().map(EmailsEntity::getId).collect(Collectors.toList()));
+        upEntity.set(EmailsEntity::getStatus, 0);
+        upEntity.in(EmailsEntity::getId, emailsls.stream().map(EmailsEntity::getId).collect(Collectors.toList()));
         emailsService.update(upEntity);
 
-        List<SmtpPopSettingsEntity> smtpPopls = smtpPopSettingsService.listByIds(emailsls.stream().map(EmailsEntity::getSmtpPopId).collect(Collectors.toList()));
+        List<SmtpPopSettingsEntity> smtpPopls = smtpPopSettingsService.listByIds(
+                emailsls.stream().map(EmailsEntity::getSmtpPopId).collect(Collectors.toList()));
         if (CollectionUtils.isEmpty(smtpPopls)) {
             return;
         }
-        Map<Long,SmtpPopSettingsEntity> smtpPopMap = smtpPopls.stream().collect(Collectors.toMap(SmtpPopSettingsEntity::getId, a ->a));
-        log.info("定时发送邮件任务,共{}条",emailsls.size());
+        Map<Long, SmtpPopSettingsEntity> smtpPopMap = smtpPopls.stream()
+                .collect(Collectors.toMap(SmtpPopSettingsEntity::getId, a -> a));
+        log.info("定时发送邮件任务,共{}条", emailsls.size());
         for (EmailsEntity emailVal : emailsls) {
             EmailsEntity email = emailsService.getById(emailVal.getId());
             SmtpPopSettingsEntity smtpPop = smtpPopMap.get(email.getSmtpPopId());
             AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
             String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
             smtpPop.setEmailPassword(pass);
-            emailsService.sendEmail(email,smtpPop);
+            emailsService.sendEmail(email, smtpPop);
         }
     }
-
-    public static void main(String[] args) {
-        String env = System.getProperty("app.env");
-        log.error("env ="+env);
-    }
 }

+ 65 - 59
storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/DelayDeleteServerEmailTaskJob.java

@@ -11,10 +11,7 @@ import com.storlead.sales.mail.service.SmtpPopSettingsService;
 import com.sun.mail.imap.IMAPFolder;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.quartz.DisallowConcurrentExecution;
-import org.quartz.Job;
-import org.quartz.JobExecutionContext;
-import org.quartz.JobExecutionException;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 
@@ -24,16 +21,21 @@ import java.util.*;
 import java.util.stream.Collectors;
 
 /**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2025-03-12 09:47
+ * 延迟删除邮件服务器上过期邮件的定时任务。
+ * <p>
+ * 按 cron 扫描待删除记录,通过 IMAP 在收件箱/发件箱中彻底删除对应邮件,
+ * 并回写本地 {@code delete_tag} 状态。执行时间由
+ * {@code storlead.mail.scheduler.delay-delete-cron} 配置(默认每天 2:00)。
+ * </p>
+ * <p>
+ * 说明:延迟<strong>发送</strong>邮件请见 {@link AutoSendDelayEmailTaskJob}。
+ * </p>
  */
 @Slf4j
 @Component
 @NoArgsConstructor
-@DisallowConcurrentExecution
-public class DelayDeleteServerEmailTaskJob implements Job {
+public class DelayDeleteServerEmailTaskJob {
+
 
     @Resource
     private SmtpPopSettingsService smtpPopSettingsService;
@@ -41,63 +43,69 @@ public class DelayDeleteServerEmailTaskJob implements Job {
     @Resource
     private EmailsService emailsService;
 
-    @Override
-    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
-        List<EmailsEntity> taskEntities =  emailsService.getDelayDeleteMail();
-        if (CollectionUtil.isNotEmpty(taskEntities)) {
-            Map<Long,List<EmailsEntity>> attachmentsMap = taskEntities.stream().collect(Collectors.groupingBy(EmailsEntity::getSmtpPopId));
-            for (Map.Entry<Long, List<EmailsEntity>> entry : attachmentsMap.entrySet()) {
-                SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getById(entry.getKey());
-                if (Objects.nonNull(smtpPop) && Integer.valueOf(0).equals(smtpPop.getIsDeleteMail())) {
-                    continue;
-                }
-                List<EmailsEntity>  entryls = entry.getValue();
-                if (!CollectionUtils.isEmpty(entryls)) {
-                    AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
-                    String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
-                    smtpPop.setEmailPassword(pass);
-                    MailProperties properties1 = new MailProperties(smtpPop);
-                    Properties properties = new Properties();
-                    properties.put("mail.imap.host", properties1.getHost());
-                    properties.put("mail.imap.port", properties1.getPort());
-                    properties.put("mail.imap.starttls.enable", "true");
-                    properties.put("mail.imap.ssl.trust", "*");
-                    properties.put("mail.imap.ssl.enable", "true");
+    /**
+     * 定时执行:从邮件服务器延迟删除已标记过期的邮件。
+     */
+    @Scheduled(cron = "${storlead.mail.scheduler.delay-delete-cron:0 0 2 * * ?}")
+    public void delayDeleteServerEmailTask() {
+        List<EmailsEntity> taskEntities = emailsService.getDelayDeleteMail();
+        if (CollectionUtil.isEmpty(taskEntities)) {
+            return;
+        }
+        Map<Long, List<EmailsEntity>> attachmentsMap = taskEntities.stream()
+                .collect(Collectors.groupingBy(EmailsEntity::getSmtpPopId));
+        for (Map.Entry<Long, List<EmailsEntity>> entry : attachmentsMap.entrySet()) {
+            SmtpPopSettingsEntity smtpPop = smtpPopSettingsService.getById(entry.getKey());
+            if (Objects.nonNull(smtpPop) && Integer.valueOf(0).equals(smtpPop.getIsDeleteMail())) {
+                continue;
+            }
+            List<EmailsEntity> entryls = entry.getValue();
+            if (CollectionUtils.isEmpty(entryls)) {
+                continue;
+            }
+            AccessKeyEncryptor accessKeyEncryptor = AccessKeyEncryptor.getAccessKeyEncryptor("");
+            String pass = accessKeyEncryptor.decrypt(smtpPop.getEmailPassword());
+            smtpPop.setEmailPassword(pass);
+            MailProperties properties1 = new MailProperties(smtpPop);
+            Properties properties = new Properties();
+            properties.put("mail.imap.host", properties1.getHost());
+            properties.put("mail.imap.port", properties1.getPort());
+            properties.put("mail.imap.starttls.enable", "true");
+            properties.put("mail.imap.ssl.trust", "*");
+            properties.put("mail.imap.ssl.enable", "true");
 
-                    Authenticator auth = new Authenticator() {
-                        protected PasswordAuthentication getPasswordAuthentication() {
-                            return new PasswordAuthentication(properties1.getEmailAddress(), properties1.getEmailPassword());
-                        }
-                    };
-                    Session session = Session.getInstance(properties,auth);
-                    List<EmailsEntity> inEntryls = entryls.stream().filter(e ->e.getFolder().equals("INBOX")).collect(Collectors.toList());
-                    if (!CollectionUtils.isEmpty(inEntryls)) {
-                        expungeMailData(inEntryls,properties1,session,"INBOX");
-                    }
-                    List<EmailsEntity> outEntryls = entryls.stream().filter(e ->e.getFolder().equals("SENT")).collect(Collectors.toList());
-                    if (!CollectionUtils.isEmpty(outEntryls)) {
-                        expungeMailData(outEntryls,properties1,session,"SENT");
-                    }
+            Authenticator auth = new Authenticator() {
+                @Override
+                protected PasswordAuthentication getPasswordAuthentication() {
+                    return new PasswordAuthentication(properties1.getEmailAddress(), properties1.getEmailPassword());
                 }
+            };
+            Session session = Session.getInstance(properties, auth);
+            List<EmailsEntity> inEntryls = entryls.stream()
+                    .filter(e -> "INBOX".equals(e.getFolder())).collect(Collectors.toList());
+            if (!CollectionUtils.isEmpty(inEntryls)) {
+                expungeMailData(inEntryls, properties1, session, "INBOX");
+            }
+            List<EmailsEntity> outEntryls = entryls.stream()
+                    .filter(e -> "SENT".equals(e.getFolder())).collect(Collectors.toList());
+            if (!CollectionUtils.isEmpty(outEntryls)) {
+                expungeMailData(outEntryls, properties1, session, "SENT");
             }
         }
     }
 
-    public void expungeMailData(List<EmailsEntity> entryls,MailProperties properties1,Session session,String folder) {
+    public void expungeMailData(List<EmailsEntity> entryls, MailProperties properties1, Session session, String folder) {
         try {
-            // 连接 IMAP 服务器
             Store store = session.getStore(properties1.getProtocol().toLowerCase());
             store.connect(properties1.getEmailAddress(), properties1.getEmailPassword());
-            // 打开 INBOX 目录(可写模式)
             if ("SENT".equals(folder)) {
                 folder = "Sent Items";
             }
-            IMAPFolder inbox = (IMAPFolder)store.getFolder(folder);
+            IMAPFolder inbox = (IMAPFolder) store.getFolder(folder);
             inbox.open(Folder.READ_WRITE);
             int totalDeleted = 0;
             List<Long> deleteIds = new ArrayList<>();
             List<Long> nullIds = new ArrayList<>();
-            // 遍历 Message-ID 进行搜索并彻底删除
             for (EmailsEntity entity : entryls) {
                 Message message = inbox.getMessageByUID(Long.parseLong(entity.getMsgUid()));
                 if (Objects.nonNull(message)) {
@@ -111,26 +119,24 @@ public class DelayDeleteServerEmailTaskJob implements Job {
             }
             if (!CollectionUtils.isEmpty(nullIds)) {
                 LambdaUpdateWrapper<EmailsEntity> nullUp = new LambdaUpdateWrapper<>();
-                nullUp.set(EmailsEntity::getDeleteTag,2);
-                nullUp.in(EmailsEntity::getId,nullIds);
+                nullUp.set(EmailsEntity::getDeleteTag, 2);
+                nullUp.in(EmailsEntity::getId, nullIds);
                 emailsService.update(nullUp);
             }
-            // 确保邮件彻底删除
             if (totalDeleted > 0) {
                 inbox.expunge();
-                if (!CollectionUtils.isEmpty(nullIds)) {
+                if (!CollectionUtils.isEmpty(deleteIds)) {
                     LambdaUpdateWrapper<EmailsEntity> deleteUp = new LambdaUpdateWrapper<>();
-                    deleteUp.set(EmailsEntity::getDeleteTag,1);
-                    deleteUp.in(EmailsEntity::getId,deleteIds);
+                    deleteUp.set(EmailsEntity::getDeleteTag, 1);
+                    deleteUp.in(EmailsEntity::getId, deleteIds);
                     emailsService.update(deleteUp);
                 }
                 log.error("已彻底删除 " + totalDeleted + " 封邮件!");
             }
-            // 关闭连接
             inbox.close(true);
             store.close();
         } catch (Exception e) {
-            e.printStackTrace();
+            log.error("expungeMailData error", e);
         }
     }
 }

+ 10 - 20
storlead-mail/src/main/java/com/storlead/sales/mail/dispatch/EmailPullHeadContentTaskJob.java

@@ -2,30 +2,21 @@ package com.storlead.sales.mail.dispatch;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.storlead.sales.mail.entity.SmtpPopSettingsEntity;
-import com.storlead.sales.mail.service.*;
+import com.storlead.sales.mail.service.EmailsService;
+import com.storlead.sales.mail.service.SmtpPopSettingsService;
 import lombok.NoArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.quartz.DisallowConcurrentExecution;
-import org.quartz.Job;
-import org.quartz.JobExecutionContext;
-import org.quartz.JobExecutionException;
+import org.springframework.scheduling.annotation.Scheduled;
 import org.springframework.stereotype.Component;
 import org.springframework.util.CollectionUtils;
 
 import javax.annotation.Resource;
 import java.util.List;
 
-/**
- * @program: sp-sales-platform
- * @description:
- * @author: chenkq
- * @create: 2024-08-27 17:52
- */
 @Slf4j
 @Component
 @NoArgsConstructor
-@DisallowConcurrentExecution
-public class EmailPullHeadContentTaskJob implements Job {
+public class EmailPullHeadContentTaskJob {
 
     @Resource
     private SmtpPopSettingsService smtpPopSettingsService;
@@ -33,19 +24,18 @@ public class EmailPullHeadContentTaskJob implements Job {
     @Resource
     private EmailsService emailsService;
 
-    @Override
-    public void execute(JobExecutionContext jobExecutionContext) throws JobExecutionException {
-
+    @Scheduled(cron = "${storlead.mail.scheduler.pull-email-cron:0 */5 * * * ?}")
+    public void pullHeadMailTask() {
         LambdaQueryWrapper<SmtpPopSettingsEntity> lambdaQueryWrapper = new LambdaQueryWrapper<>();
-        lambdaQueryWrapper.eq(SmtpPopSettingsEntity::getIsDelete,0);
-        lambdaQueryWrapper.eq(SmtpPopSettingsEntity::getEnabled,1);
-        lambdaQueryWrapper.ne(SmtpPopSettingsEntity::getEmailAddress,"system@storlead.com");
+        lambdaQueryWrapper.eq(SmtpPopSettingsEntity::getIsDelete, 0);
+        lambdaQueryWrapper.eq(SmtpPopSettingsEntity::getEnabled, 1);
+        lambdaQueryWrapper.ne(SmtpPopSettingsEntity::getEmailAddress, "system@storlead.com");
         List<SmtpPopSettingsEntity> settingsEntities = smtpPopSettingsService.list(lambdaQueryWrapper);
         if (CollectionUtils.isEmpty(settingsEntities)) {
             return;
         }
         for (SmtpPopSettingsEntity smtpPop : settingsEntities) {
-             emailsService.receiveEmails(smtpPop,true);
+            emailsService.receiveEmails(smtpPop, true);
         }
     }
 }