创新点

MyBatis-Plus 与 MyBatis 的核心区别

MyBatis-Plus(简称 MP)是 MyBatis 的增强工具,在 MyBatis 基础上提供了更简化的 CRUD 操作、代码生成器和实用插件。以下是两者的核心区别:

1. 功能定位

MyBatis

  • 基础 ORM 框架:提供 SQL 映射和执行功能,需手动编写 SQL 语句

  • 灵活性高:支持 XML、注解等多种 SQL 定义方式

  • 需编写大量样板代码:如 Mapper 接口、XML 映射文件

MyBatis-Plus

  • MyBatis 增强工具:不改变 MyBatis 原有功能,提供 "无 SQL" 开发体验

  • 简化 CRUD 操作:内置通用 Mapper 和 Service,减少重复代码

  • 自动代码生成:支持实体类、Mapper、Service 等代码自动生成

2. CRUD 操作方式

MyBatis

需要手动编写 Mapper 接口和 SQL:

 // Mapper接口
 public interface UserMapper {
     @Select("SELECT * FROM user WHERE id = #{id}")
     User selectById(Long id);
 }

MyBatis-Plus

继承BaseMapper接口即可获得基础 CRUD 方法:

 // 无需编写任何方法,直接继承BaseMapper
 public interface UserMapper extends BaseMapper<User> {
     // 可添加自定义方法
 }
 
 // 使用示例
 User user = userMapper.selectById(1L);

3. 分页查询

MyBatis

需手动编写分页 SQL 和 RowBounds:

 // XML中定义分页查询
 <select id="selectUserPage" parameterType="map" resultType="User">
     SELECT * FROM user LIMIT #{offset}, #{size}
 </select>

MyBatis-Plus

内置分页插件,直接使用Page对象:

 // 配置分页插件
 @Configuration
 public class MyBatisPlusConfig {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
         interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
         return interceptor;
    }
 }
 
 // 使用分页查询
 Page<User> page = new Page<>(1, 10);
 userMapper.selectPage(page, null);

4. 条件构造器

MyBatis

需手动拼接 SQL 条件:

 @SelectProvider(type = UserSqlProvider.class, method = "selectByCondition")
 List<User> selectByCondition(@Param("name") String name, @Param("age") Integer age);
 
 // SQL提供者
 public class UserSqlProvider {
     public String selectByCondition(String name, Integer age) {
         StringBuilder sql = new StringBuilder("SELECT * FROM user WHERE 1=1");
         if (name != null) {
             sql.append(" AND name = #{name}");
        }
         if (age != null) {
             sql.append(" AND age = #{age}");
        }
         return sql.toString();
    }
 }

MyBatis-Plus

使用QueryWrapper链式构造条件:

 QueryWrapper<User> wrapper = new QueryWrapper<>();
 wrapper.like("name", "张")
        .ge("age", 18)
        .orderByDesc("create_time");
 
 List<User> userList = userMapper.selectList(wrapper);

5. 代码生成器

MyBatis

需手动创建实体类、Mapper 接口、Service 等代码

MyBatis-Plus

内置代码生成器,可自动生成全套代码:

 // 代码生成示例
 FastAutoGenerator.create("jdbc:mysql://localhost:3306/weblog", "root", "password")
    .globalConfig(builder -> {
         builder.outputDir("src/main/java")
                .author("doubao")
                .disableOpenDir();
    })
    .packageConfig(builder -> {
         builder.parent("com.quanxiaoha.weblog")
                .moduleName("web")
                .entity("entity")
                .mapper("mapper")
                .service("service")
                .controller("controller");
    })
    .strategyConfig(builder -> {
         builder.addInclude("user", "role")
                .addTablePrefix("t_", "sys_");
    })
    .execute();

6. 插件生态

MyBatis

需手动集成分页、乐观锁等插件

MyBatis-Plus

内置常用插件:

  • 分页插件(PaginationInterceptor)

  • 乐观锁插件(OptimisticLockerInterceptor)

  • 自动填充插件(MetaObjectHandler)

  • SQL 性能分析插件(PerformanceInterceptor)

7. 适用场景

MyBatis

  • 复杂 SQL 场景(如多表关联、自定义分页)

  • 对 SQL 性能有严格要求的项目

  • 需要高度定制化的 ORM 需求

MyBatis-Plus

  • 快速开发 CRUD 应用

  • 中小型项目(减少样板代码)

  • 对 SQL 复杂度要求不高的场景

总结

特性MyBatisMyBatis-Plus
SQL 编写 手动编写全部 SQL 内置通用方法,支持无 SQL
分页实现 手动编写分页 SQL 内置分页插件
条件查询 手动拼接条件 QueryWrapper 链式调用
代码生成 需第三方工具 内置代码生成器
学习成本 较高(需掌握 SQL 映射) 较低(基于 MyBatis)
适用场景 复杂 SQL 项目 快速 CRUD 开发

建议:如果项目 SQL 逻辑简单且追求开发效率,推荐使用 MyBatis-Plus;如果需要对 SQL 进行精细控制,或项目已有成熟的 MyBatis 架构,可继续使用 MyBatis。两者可以无缝集成,你也可以在 MyBatis 基础上选择性使用 MyBatis-Plus 的部分功能。

 

项目使用 MinIO 的原因及好处

原因

在博客项目中,可能会涉及到图片、附件等文件的存储和管理,而传统的文件系统存储方式可能存在一些局限性,如扩展性差、难以管理等。因此,需要一个专业的对象存储服务来解决这些问题。MinIO 作为一款轻量级的开源对象存储服务,具有简单易用、高性能、可扩展性强等特点,非常适合用于博客项目中的文件存储。

好处

  1. 轻量级与易用性:MinIO 是一个轻量级的对象存储系统,部署和配置都相对简单,不需要复杂的基础设施和专业的运维知识。对于小型项目或者开发团队来说,能够快速上手并集成到项目中。

  2. 高性能:MinIO 设计为高性能的对象存储,能够快速处理大量的文件上传和下载请求。在博客项目中,用户可能会频繁地进行图片、附件等文件的上传和查看操作,MinIO 的高性能可以确保这些操作的快速响应,提升用户体验。

  3. 兼容性:MinIO 兼容 Amazon S3 API,这意味着在使用 MinIO 时,可以利用现有的基于 S3 API 开发的工具和库,降低了开发成本和学习成本。同时,也方便了项目的迁移和扩展。

  4. 可扩展性:MinIO 支持分布式部署,可以通过添加节点来扩展存储容量和处理能力。随着博客项目的发展,用户数量和文件数量可能会不断增加,MinIO 的可扩展性可以确保系统能够适应这种增长。

  5. 数据安全:MinIO 提供了丰富的安全功能,如访问控制、加密等,可以确保存储在其中的数据的安全性。在博客项目中,用户上传的文件可能包含敏感信息,MinIO 的安全功能可以保护这些信息不被泄露。

MinIO 在项目中的用法

1. 配置 MinIO 客户端

在项目中,需要配置 MinIO 客户端来与 MinIO 服务器进行交互。以下是项目中配置 MinIO 客户端的示例代码:

 // WebMinioConfig.java
 package com.quanxiaoha.weblog.web.config;
 
 import io.minio.MinioClient;
 import lombok.Data;
 import okhttp3.ConnectionPool;
 import okhttp3.OkHttpClient;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 import lombok.extern.slf4j.Slf4j;
 import javax.annotation.PostConstruct;
 
 import java.util.concurrent.TimeUnit;
 
 @Configuration
 @Slf4j
 public class WebMinioConfig {
 
     @Value("${minio.endpoint}")
     private String endpoint;
 
     @Value("${minio.publicEndpoint}")
     private String publicEndpoint;
 
     @Value("${minio.accessKey}")
     private String accessKey;
 
     @Value("${minio.secretKey}")
     private String secretKey;
 
     @Value("${minio.bucketName:weblog}")
     private String bucketName;
 
     @PostConstruct
     public void init() {
         log.info("=================================================================");
         log.info("Minio配置信息:");
         log.info("API端点: {}", endpoint);
         log.info("公共访问端点: {}", publicEndpoint);
         log.info("存储桶: {}", bucketName);
         log.info("=================================================================");
    }
 
     @Bean(name = "webOkHttpClient")
     public OkHttpClient okHttpClient() {
         return new OkHttpClient.Builder()
                .connectionPool(new ConnectionPool(5, 30, TimeUnit.MINUTES))
                .connectTimeout(10, TimeUnit.SECONDS)
                .readTimeout(30, TimeUnit.SECONDS)
                .writeTimeout(30, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true)
                .build();
    }
 
     @Bean(name = "webMinioClient")
     public MinioClient minioClient() {
         log.info("初始化WebMinioClient,使用端点: {}", endpoint);
         return MinioClient.builder()
                .endpoint(endpoint)
                .credentials(accessKey, secretKey)
                .region("us-east-1") // 显式设置区域
                .httpClient(okHttpClient())
                .build();
    }
 }

上述代码中,通过 @Configuration 注解将 WebMinioConfig 类标记为配置类,使用 @Value 注解从配置文件中读取 MinIO 的相关配置信息,然后使用 MinioClient.builder() 方法创建 MinIO 客户端实例。

2. 初始化 MinIO 存储桶

在应用启动时,需要确保 MinIO 存储桶已经存在,并且配置了相应的访问策略。以下是项目中初始化 MinIO 存储桶的示例代码:

 // MinioInitializer.java
 package com.quanxiaoha.weblog.web.config;
 
 import io.minio.BucketExistsArgs;
 import io.minio.MakeBucketArgs;
 import io.minio.MinioClient;
 import io.minio.SetBucketPolicyArgs;
 import io.minio.errors.ErrorResponseException;
 import io.minio.errors.MinioException;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.boot.CommandLineRunner;
 import org.springframework.core.annotation.Order;
 import org.springframework.stereotype.Component;
 
 @Component
 @Slf4j
 @Order(1) // 优先执行
 public class MinioInitializer implements CommandLineRunner {
 
     @Autowired
     @Qualifier("webMinioClient")
     private MinioClient minioClient;
 
     @Value("${minio.bucketName}")
     private String bucketName;
 
     @Override
     public void run(String... args) throws Exception {
         try {
             log.info("开始初始化Minio配置...");
             initializeBucket();
             configureBucketPolicy();
             log.info("Minio配置完成!");
        } catch (Exception e) {
             log.error("Minio初始化失败", e);
        }
    }
 
     private void initializeBucket() {
         try {
             boolean bucketExists = minioClient.bucketExists(
                     BucketExistsArgs.builder().bucket(bucketName).build());
 
             if (!bucketExists) {
                 log.info("存储桶 {} 不存在,开始创建...", bucketName);
                 minioClient.makeBucket(
                         MakeBucketArgs.builder().bucket(bucketName).build());
                 log.info("存储桶 {} 创建成功", bucketName);
            } else {
                 log.info("存储桶 {} 已存在", bucketName);
            }
        } catch (Exception e) {
             log.error("初始化存储桶失败", e);
        }
    }
 
     private void configureBucketPolicy() {
         try {
             String policy = String.format(
                     "{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":[\"*\"]},\"Action\":[\"s3:GetObject\"],\"Resource\":[\"arn:aws:s3:::%s/*\"]}]}",
                     bucketName);
 
             minioClient.setBucketPolicy(
                     SetBucketPolicyArgs.builder()
                            .bucket(bucketName)
                            .config(policy)
                            .build());
 
             log.info("已配置存储桶 {} 的公共读取权限", bucketName);
        } catch (ErrorResponseException e) {
             if (e.errorResponse().code().equals("NoSuchBucketPolicy")) {
                 log.info("存储桶 {} 无现有策略,已应用新策略", bucketName);
            } else {
                 log.error("配置存储桶策略失败", e);
            }
        } catch (Exception e) {
             log.error("配置存储桶策略失败", e);
        }
    }
 }

上述代码中,通过实现 CommandLineRunner 接口,在应用启动时自动执行 run 方法,该方法调用 initializeBucket 方法检查存储桶是否存在,如果不存在则创建存储桶,然后调用 configureBucketPolicy 方法配置存储桶的访问策略,允许公共读取。

3. 文件上传

在博客项目中,用户可能会上传图片、附件等文件,以下是项目中文件上传的示例代码:

 // MinioUtil.java
 package com.quanxiaoha.weblog.admin.utils;
 
 import com.quanxiaoha.weblog.admin.config.MinioProperties;
 import com.quanxiaoha.weblog.common.exception.BizException;
 import io.minio.BucketExistsArgs;
 import io.minio.MakeBucketArgs;
 import io.minio.MinioClient;
 import io.minio.PutObjectArgs;
 import io.minio.errors.*;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
 import java.net.ConnectException;
 import java.net.SocketException;
 import java.net.SocketTimeoutException;
 import java.security.InvalidKeyException;
 import java.security.NoSuchAlgorithmException;
 import java.util.UUID;
 
 @Component
 @Slf4j
 public class MinioUtil {
 
     private static final int MAX_RETRY_ATTEMPTS = 3;
     private static final long RETRY_DELAY_MS = 1000; // 1秒
 
     @Autowired
     private MinioProperties minioProperties;
 
     @Autowired
     private MinioClient minioClient;
 
     public void ensureBucketExists(String bucketName) throws Exception {
         try {
             log.info("==> 检查存储桶是否存在: {}, 使用端点: {}", bucketName, minioProperties.getEndpoint());
             boolean bucketExists = minioClient.bucketExists(
                     BucketExistsArgs.builder().bucket(bucketName).build());
 
             if (!bucketExists) {
                 log.info("==> 存储桶 {} 不存在,开始创建...", bucketName);
                 minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build());
                 log.info("==> 存储桶 {} 创建成功", bucketName);
            } else {
                 log.info("==> 存储桶 {} 已经存在", bucketName);
            }
        } catch (Exception e) {
             log.error("==> 检查或创建存储桶失败: {}", e.getMessage(), e);
             if (e instanceof ConnectException) {
                 throw new RuntimeException("连接Minio服务器失败,请检查Minio服务是否启动以及端口配置是否正确", e);
            } else if (e instanceof ErrorResponseException) {
                 throw new RuntimeException("Minio服务返回错误,可能是端点配置错误或权限问题: " + e.getMessage(), e);
            } else {
                 throw e;
            }
        }
    }
 
     public String uploadFile(MultipartFile file) throws Exception {
         if (file == null || file.getSize() == 0) {
             log.error("==> 上传文件异常:文件大小为空 ...");
             throw new RuntimeException("文件大小不能为空");
        }
 
         String originalFileName = file.getOriginalFilename();
         String contentType = file.getContentType();
 
         String key = UUID.randomUUID().toString().replace("-", "");
         String suffix = originalFileName.substring(originalFileName.lastIndexOf("."));
 
         String objectName = String.format("%s%s", key, suffix);
 
         Exception lastException = null;
         for (int attempt = 1; attempt <= MAX_RETRY_ATTEMPTS; attempt++) {
             try {
                 log.info("==> 尝试第 {} 次上传文件至 Minio, ObjectName: {}, Endpoint: {}",
                         attempt, objectName, minioProperties.getEndpoint());
 
                 ensureBucketExists(minioProperties.getBucketName());
 
                 log.info("==> 开始执行文件上传操作...");
                 minioClient.putObject(PutObjectArgs.builder()
                        .bucket(minioProperties.getBucketName())
                        .object(objectName)
                        .stream(file.getInputStream(), file.getSize(), -1)
                        .contentType(contentType)
                        .build());
 
                 String url = String.format("%s/%s/%s",
                         minioProperties.getPublicEndpointOrDefault(),
                         minioProperties.getBucketName(),
                         objectName);
 
                 log.info("==> 上传文件至 Minio 成功,访问路径: {}", url);
                 return url;
 
            } catch (SocketException | SocketTimeoutException e) {
                 lastException = e;
                 log.warn("==> 上传文件至 Minio 失败 (尝试 {}/{}): 网络连接问题: {}",
                         attempt, MAX_RETRY_ATTEMPTS, e.getMessage());
 
                 if (attempt < MAX_RETRY_ATTEMPTS) {
                     try {
                         Thread.sleep(RETRY_DELAY_MS * attempt);
                    } catch (InterruptedException ie) {
                         Thread.currentThread().interrupt();
                         throw new RuntimeException("上传被中断", ie);
                    }
                }
            } catch (Exception e) {
                 log.error("==> 上传文件至 Minio 失败:{}, 错误类型: {}", e.getMessage(), e.getClass().getName(), e);
 
                 if (e instanceof ErrorResponseException) { 
                   ErrorResponseException ere = (ErrorResponseException) e; 
                   log.error("==> Minio错误响应: {}, 状态码: {}", ere.getMessage(), ere.response().code()); 
                   throw new RuntimeException("Minio服务返回错误: " + ere.getMessage(), e); 
              } else { 
                   throw e; 
              } 
          } 
      } 
​ 
       log.error("==> 上传文件至 Minio 失败:重试 {} 次后仍然失败", MAX_RETRY_ATTEMPTS); 
       throw lastException != null ? lastException : new RuntimeException("上传文件失败,请稍后重试"); 
  } 
}

上述代码中,MinioUtil 类封装了文件上传的相关操作,ensureBucketExists 方法用于确保存储桶存在,uploadFile 方法用于上传文件,并处理了网络异常的重试逻辑。

4. URL 转换

在项目中,可能需要将 MinIO 的 API 端点 URL 转换为公共访问端点 URL,以确保前端能够正确访问文件。以下是项目中 URL 转换的示例代码:

 // MinioUrlConverter.java
 package com.quanxiaoha.weblog.web.utils;
 
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Component;
 import org.springframework.util.StringUtils;
 
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 @Component
 @Slf4j
 public class MinioUrlConverter {
 
     @Value("${minio.endpoint}")
     private String apiEndpoint;
 
     @Value("${minio.publicEndpoint}")
     private String publicEndpoint;
 
     @Value("${minio.bucketName:weblog}")
     private String bucketName;
 
     @Value("${minio.replaceLocalhost:false}")
     private boolean replaceLocalhost;
 
     @Value("${minio.localhostReplacement:}")
     private String localhostReplacement;
 
     private static final Pattern PORT_PATTERN = Pattern.compile(":(\\d+)");
 
     public String convert(String url) {
         if (StringUtils.isEmpty(url)) {
             log.debug("URL为空,不进行转换");
             return url;
        }
 
         log.info("开始转换URL: {}, apiEndpoint={}, publicEndpoint={}", url, apiEndpoint, publicEndpoint);
         String convertedUrl = url;
 
         try {
             if (!url.startsWith("http")) {
                 convertedUrl = handleRelativePath(url);
                 log.info("相对路径转换结果: {} -> {}", url, convertedUrl);
                 return handleLocalhostReplacement(convertedUrl);
            }
 
             int urlPort = extractPort(url);
             int apiPort = extractPort(apiEndpoint);
             int publicPort = extractPort(publicEndpoint);
 
             log.debug("URL端口分析: URL端口={}, API端口={}, 公共端口={}", urlPort, apiPort, publicPort);
 
             if (urlPort == apiPort && apiPort != publicPort) {
                 convertedUrl = replacePort(url, apiPort, publicPort);
                 log.info("端口替换: {} -> {} ({}->{})", url, convertedUrl, apiPort, publicPort);
            } else if (!url.contains(publicEndpoint) && url.contains(apiEndpoint)) {
                 convertedUrl = url.replace(apiEndpoint, publicEndpoint);
                 log.info("端点替换: {} -> {}", url, convertedUrl);
            }
 
             if (url.contains(":9005/")) {
                 convertedUrl = url.replace(":9005/", ":9000/");
                 log.info("特定端口替换(9005->9000): {} -> {}", url, convertedUrl);
            }
        } catch (Exception e) {
             log.error("URL转换发生错误: {}", e.getMessage(), e);
             return url;
        }
 
         if (convertedUrl.equals(url)) {
             log.debug("URL未发生变化,可能是因为端点配置一致或URL不需要转换");
        }
 
         return handleLocalhostReplacement(convertedUrl);
    }
 
     private int extractPort(String urlStr) {
         try {
             if (urlStr.startsWith("http")) {
                 URL url = new URL(urlStr);
                 return url.getPort() != -1 ? url.getPort() : url.getDefaultPort();
            } else {
                 Matcher matcher = PORT_PATTERN.matcher(urlStr);
                 if (matcher.find()) {
                     return Integer.parseInt(matcher.group(1));
                }
            }
        } catch (MalformedURLException e) {
             log.debug("提取端口失败: {}", e.getMessage());
        } catch (NumberFormatException e) {
             log.debug("端口号格式不正确: {}", e.getMessage());
        }
 
         return urlStr.startsWith("https") ? 443 : 80;
    }
 
     private String replacePort(String urlStr, int oldPort, int newPort) {
         try {
             URL url = new URL(urlStr);
             String protocol = url.getProtocol();
             String host = url.getHost();
             String path = url.getPath();
             String query = url.getQuery();
 
             StringBuilder newUrl = new StringBuilder();
             newUrl.append(protocol).append("://").append(host).append(":").append(newPort);
 
             if (path != null) {
                 newUrl.append(path);
            }
 
             if (query != null) {
                 newUrl.append("?").append(query);
            }
 
             return newUrl.toString();
        } catch (MalformedURLException e) { 
           log.error("替换端口失败: {}", e.getMessage()); 
           return urlStr.replace(":" + oldPort, ":" + newPort); 
      } 
  } 
​ 
   private String handleRelativePath(String url) { 
       String formattedUrl = url; 
​ 
       if (url.startsWith("/")) { 
           formattedUrl = url.substring(1); 
      } 
​ 
       if (!formattedUrl.startsWith(bucketName + "/")) { 
           formattedUrl = bucketName + "/" + formattedUrl; 
      } 
​ 
       return publicEndpoint + "/" + formattedUrl; 
  } 
​ 
   private String handleLocalhostReplacement(String url) { 
       if (replaceLocalhost && !StringUtils.isEmpty(localhostReplacement) && 
              (url.contains("localhost") || url.contains("127.0.0.1"))) { 
           String finalUrl = url 
                  .replace("localhost", localhostReplacement) 
                  .replace("127.0.0.1", localhostReplacement); 
           log.info("替换localhost/127.0.0.1: {} -> {}", url, finalUrl); 
           return finalUrl; 
      } 
       return url; 
  } 
}

上述代码中,MinioUrlConverter 类封装了 URL 转换的相关操作,convert 方法用于将 MinIO 的 API 端点 URL 转换为公共访问端点 URL,处理了相对路径、端口替换、localhost 替换等情况。

5. 前端处理

在前端项目中,可能需要处理 MinIO 图片 URL 的修复和懒加载问题。以下是项目中前端处理的示例代码:

 // minioImageFixer.js
 /**
  * Minio图片URL修复工具
  * 解决Minio图片端口不一致问题,确保前端能正确显示图片
  */
 
 /**
  * 修复Minio图片URL
  * 如果URL包含9000端口但图片无法显示,尝试替换为9005端口
  * 如果URL包含9005端口但图片无法显示,尝试替换为9000端口
  * @param {string} url 原始图片URL
  * @returns {string} 修复后的URL
  */
 export function fixMinioImageUrl(url) {
   if (!url) return url;
   
   // 检查URL是否是Minio图片URL(包含weblog路径)
   if (url.includes('/weblog/')) {
     // 尝试从9000端口切换到9005端口
     if (url.includes(':9000/')) {
       console.log('修复Minio图片URL:9000 -> 9005', url);
       return url.replace(':9000/', ':9005/');
    }
     
     // 尝试从9005端口切换到9000端口
     if (url.includes(':9005/')) {
       console.log('修复Minio图片URL:9005 -> 9000', url);
       return url.replace(':9005/', ':9000/');
    }
  }
   
   return url;
 }
 
 /**
  * 懒加载图片处理,在图片加载失败时尝试使用另一个端口
  * @param {Event} event 图片加载失败事件
  */
 export function handleImageError(event) {
   const img = event.target;
   const originalSrc = img.src;
   
   // 防止无限重试
   if (img.dataset.retried) {
     console.log('图片已尝试过修复,但仍然失败', originalSrc);
     return;
  }
   
   // 标记已尝试修复
   img.dataset.retried = 'true';
   
   // 尝试修复URL
   if (originalSrc.includes(':9000/')) {
     img.src = originalSrc.replace(':9000/', ':9005/');
     console.log('图片加载失败,尝试使用9005端口', img.src);
  } else if (originalSrc.includes(':9005/')) {
     img.src = originalSrc.replace(':9005/', ':9000/');
     console.log('图片加载失败,尝试使用9000端口', img.src);
  }
 }
 
 /**
  * 全局图片URL修复指令(用于Vue指令)
  */
 export const MinioImageDirective = {
   mounted(el, binding) {
     // 如果是img标签
     if (el.tagName === 'IMG') {
       // 备份原始src
       const originalSrc = el.getAttribute('src');
       
       // 设置修复后的URL
       if (originalSrc) {
         el.setAttribute('src', fixMinioImageUrl(originalSrc));
      }
       
       // 添加错误处理
       el.addEventListener('error', handleImageError);
    }
     
     // 处理背景图片
     const style = getComputedStyle(el);
     if (style.backgroundImage) {
       const match = style.backgroundImage.match(/url\(['"]?([^'"]+)['"]?\)/);
       if (match && match[1]) {
         const originalUrl = match[1];
         const fixedUrl = fixMinioImageUrl(originalUrl);
         if (originalUrl !== fixedUrl) {
           el.style.backgroundImage = `url('${fixedUrl}')`;
        }
      }
    }
  },
   
   // 更新时再次检查
   updated(el, binding) {
     if (el.tagName === 'IMG') {
       const currentSrc = el.getAttribute('src');
       el.setAttribute('src', fixMinioImageUrl(currentSrc));
    }
  },
   
   // 组件卸载前移除事件监听
   beforeUnmount(el) {
     if (el.tagName === 'IMG') {
       el.removeEventListener('error', handleImageError);
    }
  }
 };
 
 export default {
   fixMinioImageUrl,
   handleImageError,
   MinioImageDirective
 };

上述代码中,fixMinioImageUrl 方法用于修复 MinIO 图片 URL,handleImageError 方法用于处理图片加载失败事件,MinioImageDirective 是一个 Vue 指令,用于在组件挂载、更新和卸载时处理图片 URL 的修复和错误处理。

综上所述,MinIO 在博客项目中的用法主要包括配置客户端、初始化存储桶、文件上传、URL 转换和前端处理等方面,通过这些操作可以实现文件的高效存储和管理。

 

 

创新点一:基于MyBatis-Plus的动态条件查询与智能分页实现

优化叙述 : 采用MyBatis-Plus框架实现了高度灵活的动态查询机制,通过Lambda表达式构建器实现无SQL侵入式的条件拼接,结合内置分页插件实现高性能数据分页。该方案支持多条件组合查询、动态日期范围筛选和关键词模糊搜索,同时保持代码的可维护性和扩展性。

核心实现模块 :

  • AdminArticleDaoImpl.java

    • queryArticlePageList :实现文章多条件分页查询

     @Override
     public Page<ArticleDO>
     queryArticlePageList(Long current,
     Long size, Date startDate, Date
     endDate, String searchTitle) {
        Page<ArticleDO> page = new Page<>
        (current, size);
        QueryWrapper<ArticleDO> wrapper =
        new QueryWrapper<>();
        wrapper.lambda()
                .like(Objects.nonNull
                (searchTitle),
                ArticleDO::getTitle,
                searchTitle)
                .ge(Objects.nonNull
                (startDate),
                ArticleDO::getCreateTime,
                startDate)
                .le(Objects.nonNull
                (endDate),
                ArticleDO::getCreateTime,
                endDate)
                .orderByDesc
                (ArticleDO::getCreateTime)
                ;
        return articleMapper.selectPage
        (page, wrapper);
     }
  • AdminTagDaoImpl.java

    • queryTagPageList :标签分页查询实现

  • AdminCategoryServiceImpl.java

    • queryCategoryPageList :分类动态条件查询

创新点二:基于MinIO的高性能图片存储与URL访问控制

优化叙述 : 集成MinIO对象存储服务实现分布式图片存储,通过自定义URL转换器实现API端点与公共访问端点的智能转换,确保图片资源安全高效访问。系统自动处理图片URL的生成与转换,支持海量图片存储与高并发访问场景,同时提供灵活的访问权限控制。

核心实现模块 :

  • AdminArticleServiceImpl.java

    • convertUrl :MinIO URL转换核心方法

     private String convertUrl(String url) 
     {
        if (url == null || url.isEmpty())
        {
            return url;
        }
     
        // 如果URL包含API端点,则替换为公共访
        问端点
        if (url.contains(apiEndpoint)) {
            String convertedUrl = url.
            replace(apiEndpoint,
            publicEndpoint);
            log.debug("转换Minio URL: {}
            -> {}", url, convertedUrl);
            return convertedUrl;
        }
     
        return url;
     }
  • ArticleServiceImpl.java

    • 使用MinioUrlConverter处理文章封面图URL转换

  • ArchiveServiceImpl.java

    • 处理归档页面文章图片URL转换

posted @ 2025-06-17 17:35  joiny-  阅读(11)  评论(0)    收藏  举报