SpringBoot实现图片添加水印(完整)

提示:昨天不是写了一个类似与图片添加水印的版本吗,今天来写一个带数据库,并且可以完整访问的版本

文章目录

目录

文章目录

引入库

配置文件

数据库配置

字段配置

 索引配置

 数据库表语句

启动文件

 前端代码

整体代码目录

 配置类AppConfig

Controller层

ABaseController

AGlobalExceptionHandlerController

TestController

Constants

dto

enums

DateTimePatternEnum

FileStatusEnum

FileTypeEnum

ResponseCodeEnum

VO

FileInfoVo

ResponseVO

异常处理器

Mappers

FileInfoMapper

FileInfoMapper.xml

Service

UploadService

UploadServiceImpl

工具类

FileUtils

StringUtil

效果

 不带水印访问

 带水印访问

 总结



引入库

 <dependencies>
       <!-- 引入Thymeleaf模板引擎的starter,用于前端页面的渲染 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <!-- 引入Web starter,包含Spring Web MVC等,用于构建Web应用 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- 引入MyBatis与Spring Boot的集成starter,简化MyBatis的配置与使用 -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <!-- 引入MySQL驱动,用于数据库连接 -->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- 引入Lombok,提供注解方式简化JavaBean的创建与维护,设置为optional,非必须依赖 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- 引入Spring Boot的测试starter,包含测试相关依赖,用于单元测试和集成测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- 引入Commons IO,提供文件操作相关的工具类 -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.11.0</version>
        </dependency>

        <!-- 引入thumbnailator,用于图片缩放、裁剪等操作 -->
        <dependency>
            <groupId>net.coobird</groupId>
            <artifactId>thumbnailator</artifactId>
            <version>0.4.8</version>
        </dependency>

        <!-- 引入Apache Commons Lang,提供一些Java语言工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.12.0</version>
        </dependency>
    </dependencies>

配置文件

# 应用服务 WEB 访问端口
server.port=8080
# 路径前缀
server.servlet.context-path=/api  

# 项目目录
project.folder=文件存放的路径

# 数据库配置

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/shuiyin?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.hikari.pool-name=HikariCPDatasource
spring.datasource.hikari.minimum-idle=5
spring.datasource.hikari.idle-timeout=180000
spring.datasource.hikari.maximum-pool-size=10
spring.datasource.hikari.auto-commit=true
spring.datasource.hikari.max-lifetime=1800000
spring.datasource.hikari.connection-timeout=30000
spring.datasource.hikari.connection-test-query=SELECT 1

数据库配置

字段配置

 索引配置

 

 数据库表语句

CREATE TABLE `file_info` (
  `file_id` varchar(15) COLLATE utf8mb4_general_ci NOT NULL COMMENT '文件id',
  `file_name` varchar(150) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件名字',
  `file_type` tinyint(1) DEFAULT NULL COMMENT '文件类型 0:png 1:jpg 2:jpeg 3:webp 4:gif',
  `file_size` bigint DEFAULT NULL COMMENT '文件大小',
  `file_path` varchar(255) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件路径',
  `file_md5` varchar(32) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '文件md5',
  `status` tinyint(1) DEFAULT NULL COMMENT '状态 0:禁用  1:启用',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  PRIMARY KEY (`file_id`),
  UNIQUE KEY `idx_file_id` (`file_id`) USING BTREE,
  UNIQUE KEY `idx_file_name` (`file_name`) USING BTREE,
  UNIQUE KEY `idx_file_md5` (`file_md5`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

启动文件

@SpringBootApplication
@MapperScan("com.hhh.mappers")
@EnableTransactionManagement
public class MysApplication {
    public static void main(String[] args) {
        SpringApplication.run(MysApplication.class, args);
    }

}

 前端代码

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>upload</title>
</head>
<body>
<h1>上传图片</h1>
<form method="post" enctype="multipart/form-data" action="http://localhost:8080/api/upload">
    <input type="file" name="file" />
    <button type="submit">上传</button>
</form>
</body>
</html>

整体代码目录

 配置类AppConfig

 这段代码主要是为了获取项目目录的


@Component
public class AppConfig {
    @Value("${project.folder:}")
    private String projectFolder;
    public String getProjectFolder() {
        return projectFolder;
    }
}

Controller层

ABaseController

基础控制器类,提供通用的响应构建方法

/**
 * 基础控制器类,提供通用的响应构建方法。
 */
public class ABaseController {

    /**
     * 表示操作成功的状态码。
     */
    protected static final String STATUC_SUCCESS = "success";

    /**
     * 表示操作失败或出现错误的状态码。
     */
    protected static final String STATUC_ERROR = "error";

    /**
     * 日志记录器,用于记录控制器类的运行时日志。
     */
    protected static final Logger logger = LoggerFactory.getLogger(ABaseController.class);

    /**
     * 构建一个表示操作成功的响应。
     *
     * @param t 返回的数据对象。
     * @param <T> 数据对象的类型。
     * @return 带有成功状态的响应对象。
     */
    protected <T> ResponseVO getSuccessResponseVO(T t) {
        ResponseVO<T> responseVO = new ResponseVO<>();
        responseVO.setStatus(STATUC_SUCCESS);
        responseVO.setCode(ResponseCodeEnum.CODE_200.getCode());
        responseVO.setInfo(ResponseCodeEnum.CODE_200.getMsg());
        responseVO.setData(t);
        return responseVO;
    }

    /**
     * 构建一个表示业务错误的响应。
     *
     * @param e 业务异常对象,包含错误代码和错误信息。
     * @param t 返回的数据对象。
     * @param <T> 数据对象的类型。
     * @return 带有业务错误状态的响应对象。
     */
    protected <T> ResponseVO getBusinessErrorResponseVO(BusinessException e, T t) {
        ResponseVO vo = new ResponseVO();
        vo.setStatus(STATUC_ERROR);
        if (e.getCode() == null) {
            vo.setCode(ResponseCodeEnum.CODE_600.getCode());
        } else {
            vo.setCode(e.getCode());
        }
        vo.setInfo(e.getMessage());
        vo.setData(t);
        return vo;
    }

    /**
     * 构建一个表示服务器错误的响应。
     *
     * @param t 返回的数据对象。
     * @param <T> 数据对象的类型。
     * @return 带有服务器错误状态的响应对象。
     */
    protected <T> ResponseVO getServerErrorResponseVO(T t) {
        ResponseVO vo = new ResponseVO();
        vo.setStatus(STATUC_ERROR);
        vo.setCode(ResponseCodeEnum.CODE_500.getCode());
        vo.setInfo(ResponseCodeEnum.CODE_500.getMsg());
        vo.setData(t);
        return vo;
    }

}

AGlobalExceptionHandlerController

全局异常处理控制器,继承自ABaseController,用于处理应用程序抛出的异常。
/**
 * 全局异常处理控制器,继承自ABaseController,用于处理应用程序抛出的异常。
 * 使用@RestControllerAdvice注解标识这是一个全局异常处理类。
 */
@RestControllerAdvice
public class AGlobalExceptionHandlerController extends ABaseController {

    private static final Logger logger = LoggerFactory.getLogger(AGlobalExceptionHandlerController.class);

    /**
     * 处理所有类型的异常。
     * @param e 抛出的异常对象。
     * @param request HTTP请求对象,用于获取请求URL。
     * @return 返回一个封装了异常信息的ResponseVO对象。
     */
    @ExceptionHandler(value = Exception.class)
    Object handleException(Exception e, HttpServletRequest request) {
        // 记录异常信息到日志
        logger.error("请求错误,请求地址{},错误信息:", request.getRequestURL(), e);

        ResponseVO ajaxResponse = new ResponseVO();

        // 根据不同的异常类型设置响应码和信息
        // 404 - 请求未找到
        if (e instanceof NoHandlerFoundException) {
            ajaxResponse.setCode(ResponseCodeEnum.CODE_404.getCode());
            ajaxResponse.setInfo(ResponseCodeEnum.CODE_404.getMsg());
            ajaxResponse.setStatus(STATUC_ERROR);
        } else if (e instanceof BusinessException) {
            // 业务异常
            // 业务错误
            BusinessException biz = (BusinessException) e;
            ajaxResponse.setCode(biz.getCode() == null ? ResponseCodeEnum.CODE_600.getCode() : biz.getCode());
            ajaxResponse.setInfo(biz.getMessage());
            ajaxResponse.setStatus(STATUC_ERROR);
        } else if (e instanceof BindException || e instanceof MethodArgumentTypeMismatchException) {
            // 参数绑定异常或参数类型不匹配异常
            // 参数类型错误
            ajaxResponse.setCode(ResponseCodeEnum.CODE_600.getCode());
            ajaxResponse.setInfo(ResponseCodeEnum.CODE_600.getMsg());
            ajaxResponse.setStatus(STATUC_ERROR);
        } else if (e instanceof DuplicateKeyException) {
            // 数据库主键重复异常
            // 主键冲突
            ajaxResponse.setCode(ResponseCodeEnum.CODE_601.getCode());
            ajaxResponse.setInfo(ResponseCodeEnum.CODE_601.getMsg());
            ajaxResponse.setStatus(STATUC_ERROR);
        } else {
            // 其他未指定的异常
            ajaxResponse.setCode(ResponseCodeEnum.CODE_500.getCode());
            ajaxResponse.setInfo(ResponseCodeEnum.CODE_500.getMsg());
            ajaxResponse.setStatus(STATUC_ERROR);
        }

        return ajaxResponse;
    }
}

TestController

主体控制器类

@RestController
public class TestController extends ABaseController {

    @Resource
    private UploadService uploadService;

    @Resource
    private AppConfig appConfig;

    @RequestMapping("/upload")
    public ResponseVO upload(@RequestParam("file")MultipartFile file) throws IOException, NoSuchAlgorithmException {
        if(null == file){
            throw new BusinessException("上传文件不能为空");
        }
        FileInfoVo fileinfoVo = uploadService.upload(file);
        return getSuccessResponseVO(fileinfoVo);
    }

    /**
     * 获取不带水印版本的图片
     * @param fileId
     * @param response
     */
    @RequestMapping("/getFileInfo/{fileId}")
    public void getFileInfo(@PathVariable("fileId") String fileId, HttpServletResponse response){
        FileInfoVo fileInfo = uploadService.getFileInfo(fileId);
        response.setContentType("image/jpg");
        String filePath = fileInfo.getFilePath();
        FileUtils.readFile(response, appConfig.getProjectFolder()+filePath);
    }

    /**
     * 获取带水印版本的图片
     * @param fileId
     * @param response
     */
    @RequestMapping("/getWatermarkFileInfo/{fileId}")
    public void getWatermarkFileInfo(@PathVariable("fileId") String fileId, HttpServletResponse response){
        FileInfoVo fileInfo = uploadService.getFileInfo(fileId);
        response.setContentType("image/jpg");
        String filePath = fileInfo.getFilePath();
        System.out.println(filePath);
        int dotIndex = filePath.lastIndexOf(".");
        if(dotIndex != -1){
           filePath = filePath.substring(0,dotIndex)+"_."+filePath.substring(dotIndex+1);
        }
        FileUtils.readFile(response, appConfig.getProjectFolder()+filePath);
    }
}

Constants

常量代码

/**
 * Constants类用于定义应用程序中使用的常量。
 * 该类中的常量应该是整个应用程序范围内不变的值。
 */
public class Constants {
    /**
     * 文件路径常量。
     * 该常量定义了访问文件系统的根路径。
     * 使用此常量可以确保应用程序中对文件路径的引用具有一致性和可维护性。
     */
    public static final String FILE_PATH = "/file";
}

dto

文件信息类

package com.hhh.entity.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;

public class FileInfoDto {
    private String fileId;
    private String fileName;
    private Integer fileType;
    private Long fileSize;
    private String filePath;
    private String fileMd5;
    private Integer status;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createTime;

    public String getFileId() {
        return fileId;
    }

    public void setFileId(String fileId) {
        this.fileId = fileId;
    }

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public Integer getFileType() {
        return fileType;
    }

    public void setFileType(Integer fileType) {
        this.fileType = fileType;
    }

    public Long getFileSize() {
        return fileSize;
    }

    public void setFileSize(Long fileSize) {
        this.fileSize = fileSize;
    }

    public String getFilePath() {
        return filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public String getFileMd5() {
        return fileMd5;
    }

    public void setFileMd5(String fileMd5) {
        this.fileMd5 = fileMd5;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }

    public Date getCreateTime() {
        return createTime;
    }

    public void setCreateTime(Date createTime) {
        this.createTime = createTime;
    }
}

enums

枚举

DateTimePatternEnum

public enum DateTimePatternEnum {
    YYYY_MM("yyyyMM");
    private String pattern;
    public String getPattern() {
        return pattern;
    }

    public void setPattern(String pattern) {
        this.pattern = pattern;
    }

    DateTimePatternEnum(String pattern) {
        this.pattern = pattern;
    }
}

FileStatusEnum

public enum FileStatusEnum {
    ZERO(0,"禁用"),
    ONE(1,"启用");

    private Integer code;
    private String status;

    FileStatusEnum(Integer code, String status) {
        this.code = code;
        this.status = status;
    }

    public static Integer getByCode(Integer code) {
        for (FileStatusEnum fileStatusEnum : FileStatusEnum.values()) {
            if (fileStatusEnum.getCode().equals(code)) {
                return fileStatusEnum.getCode();
            }
        }
        return null;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getStatus() {
        return status;
    }

    public void setStatus(String status) {
        this.status = status;
    }
}

FileTypeEnum

public enum FileTypeEnum {

    PNG(0,"png"),
    JPG(1,"jpg"),
    JPEG(2,"jpeg"),
    WEBP(3,"webp"),
    GIF(4,"gif");

    private Integer code;
    private String type;

    FileTypeEnum(Integer code, String type) {
        this.code = code;
        this.type = type;
    }

    public static Integer getByCode(String suffix){
        for (FileTypeEnum fileTypeEnum : FileTypeEnum.values()) {
            if (fileTypeEnum.getType().equals(suffix)) {
                return fileTypeEnum.getCode();
            }
        }
        return null;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }
}

ResponseCodeEnum

/**
 * 响应码枚举类,用于定义系统中各种操作的返回码及其对应的信息。
 */
public enum ResponseCodeEnum {
    // 请求成功
    CODE_200(200, "请求成功"),
    // 请求的资源不存在
    CODE_404(404, "请求地址不存在"),
    // 请求参数错误
    CODE_600(600, "请求参数错误"),
    // 信息已存在,通常用于数据重复的场景
    CODE_601(601, "信息已经存在"),
    // 服务器内部错误,需要管理员处理
    CODE_500(500, "服务器返回错误,请联系管理员");

    // 响应码
    private Integer code;
    // 响应信息
    private String msg;

    /**
     * 构造方法,用于初始化枚举值。
     * @param code 响应码
     * @param msg 响应信息
     */
    ResponseCodeEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 获取响应码。
     * @return 响应码
     */
    public Integer getCode() {
        return code;
    }

    /**
     * 获取响应信息。
     * @return 响应信息
     */
    public String getMsg() {
        return msg;
    }
}

VO

FileInfoVo

public class FileInfoVo {
    private String fileName;
    private String filePath;
    private Long fileSize;
    private String fileId;
    private Integer status;

    public String getFileName() {
        return fileName;
    }

    public void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public String getFilePath() {
        return filePath;
    }

    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public Long getFileSize() {
        return fileSize;
    }

    public void setFileSize(Long fileSize) {
        this.fileSize = fileSize;
    }

    public String getFileId() {
        return fileId;
    }

    public void setFileId(String fileId) {
        this.fileId = fileId;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }
}

ResponseVO

/**
 * 响应数据的通用包装类,用于封装接口调用的返回结果。
 * <p>
 * 该类提供了对响应状态、响应码、响应信息和响应数据的封装,适用于各种接口返回数据的统一格式化。
 * 通过泛型T的支持,可以灵活地携带各种类型的响应数据。
 *
 * @param <T> 响应数据的类型,使用泛型提供类型安全性和灵活性。
 */
public class ResponseVO<T> {
    /**
     * 响应的状态,用于表示接口调用的总体状态,例如"success"表示成功。
     */
    private String status;

    /**
     * 响应码,用于更详细地表示接口调用的结果状态,例如200表示成功。
     */
    private Integer code;

    /**
     * 响应信息,用于对响应状态进行描述,例如"操作成功"。
     */
    private String info;

    /**
     * 响应数据,接口调用的实际返回数据,其类型由泛型T指定。
     */
    private T data;

    /**
     * 获取响应的状态。
     *
     * @return 响应的状态字符串。
     */
    public String getStatus() {
        return status;
    }

    /**
     * 设置响应的状态。
     *
     * @param status 响应的状态字符串。
     */
    public void setStatus(String status) {
        this.status = status;
    }

    /**
     * 获取响应码。
     *
     * @return 响应的码值。
     */
    public Integer getCode() {
        return code;
    }

    /**
     * 设置响应码。
     *
     * @param code 响应的码值。
     */
    public void setCode(Integer code) {
        this.code = code;
    }

    /**
     * 获取响应信息。
     *
     * @return 响应的信息字符串。
     */
    public String getInfo() {
        return info;
    }

    /**
     * 设置响应信息。
     *
     * @param info 响应的信息字符串。
     */
    public void setInfo(String info) {
        this.info = info;
    }

    /**
     * 获取响应数据。
     *
     * @return 响应的数据对象,其类型为泛型T。
     */
    public T getData() {
        return data;
    }

    /**
     * 设置响应数据。
     *
     * @param data 响应的数据对象,其类型为泛型T。
     */
    public void setData(T data) {
        this.data = data;
    }
}

异常处理器

/**
 * 业务异常类,用于表示在业务逻辑执行过程中发生的异常情况。
 * 继承自RuntimeException,因为它是一种非检查(Unchecked)异常,可以不强制在方法签名中声明。
 * 这使得业务异常的使用更加灵活,能够更准确地反映业务逻辑中的错误情况。
 */
public class BusinessException extends RuntimeException {

    /**
     * 错误代码枚举,用于标准化错误代码和错误消息的映射。
     */
    private ResponseCodeEnum codeEnum;

    /**
     * 错误代码,用于标识具体的错误类型。
     */
    private Integer code;

    /**
     * 错误消息,用于描述错误的具体信息。
     */
    private String message;

    /**
     * 带有错误消息和原因的构造函数。
     *
     * @param message 错误消息
     * @param e 异常原因
     */
    public BusinessException(String message, Throwable e) {
        super(message, e);
        this.message = message;
    }

    /**
     * 带有错误消息的构造函数。
     *
     * @param message 错误消息
     */
    public BusinessException(String message) {
        super(message);
        this.message = message;
    }

    /**
     * 带有原因的构造函数。
     *
     * @param e 异常原因
     */
    public BusinessException(Throwable e) {
        super(e);
    }

    /**
     * 使用错误代码枚举构造业务异常。
     *
     * @param codeEnum 错误代码枚举,包含错误代码、错误消息等信息。
     */
    public BusinessException(ResponseCodeEnum codeEnum) {
        super(codeEnum.getMsg());
        this.codeEnum = codeEnum;
        this.code = codeEnum.getCode();
        this.message = codeEnum.getMsg();
    }

    /**
     * 带有错误代码和错误消息的构造函数。
     *
     * @param code 错误代码
     * @param message 错误消息
     */
    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }

    /**
     * 获取错误代码枚举。
     *
     * @return 错误代码枚举
     */
    public ResponseCodeEnum getCodeEnum() {
        return codeEnum;
    }

    /**
     * 获取错误代码。
     *
     * @return 错误代码
     */
    public Integer getCode() {
        return code;
    }

    /**
     * 获取错误消息。
     *
     * @return 错误消息
     */
    @Override
    public String getMessage() {
        return message;
    }

    /**
     * 重写fillInStackTrace方法,返回当前异常实例。
     * 业务异常中通常不需要堆栈跟踪信息,因此这个重写方法用于提高异常处理的性能。
     *
     * @return 当前异常实例
     */
    /**
     * 重写fillInStackTrace 业务异常不需要堆栈信息,提高效率.
     */
    @Override
    public Throwable fillInStackTrace() {
        return this;
    }
}

Mappers

FileInfoMapper

@Mapper
public interface FileInfoMapper {

    FileInfoVo selectByFileId(String fileId);

    void insert(FileInfoDto fileInfoDto);

    FileInfoDto selectByFileMd5(String fileMd5);
}

FileInfoMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hhh.mappers.FileInfoMapper">
    <select id="selectByFileId" resultType="com.hhh.entity.vo.FileInfoVo">
        select file_name as fileName, file_path as filePath, file_size as fileSize, file_id as fileId,status as status
        from file_info
        where file_id = #{fileId}
    </select>

    <select id="selectByFileMd5" resultType="com.hhh.entity.dto.FileInfoDto">
        select file_name as fileName, file_path as filePath, file_size as fileSize, file_id as fileId, file_type as fileType, file_md5 as fileMd5, status as status, create_time as createTime
        from file_info
        where file_md5 = #{fileMd5}
    </select>

    <insert id="insert">
        insert into file_info
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="fileId != null">file_id,</if>
            <if test="fileName != null">file_name,</if>
            <if test="filePath != null">file_path,</if>
            <if test="fileType != null">file_type,</if>
            <if test="fileSize != null">file_size,</if>
            <if test="fileMd5 != null">file_md5,</if>
            <if test="status != null">status,</if>
            <if test="createTime != null">create_time,</if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="fileId != null">#{fileId},</if>
            <if test="fileName != null">#{fileName},</if>
            <if test="filePath != null">#{filePath},</if>
            <if test="fileType != null">#{fileType},</if>
            <if test="fileSize != null">#{fileSize},</if>
            <if test="fileMd5 != null">#{fileMd5},</if>
            <if test="status != null">#{status},</if>
            <if test="createTime != null">#{createTime},</if>
        </trim>
    </insert>
</mapper>

Service

UploadService

public interface UploadService {
    FileInfoVo upload(MultipartFile file) throws IOException, NoSuchAlgorithmException;

    FileInfoVo getFileInfo(String fileId);
}

UploadServiceImpl

@Service("UploadService")
public class UploadServiceImpl implements UploadService {

    private static final Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);

    private static final List<String> ALLOWED_EXTENSIONS = Arrays.asList("jpg", "jpeg", "png", "gif", "webp");


    @Resource
    private AppConfig appConfig;

    @Resource
    private FileInfoMapper fileInfoMapper;


    @Override
    @Transactional(rollbackFor = Exception.class)
    public FileInfoVo upload(MultipartFile file) throws IOException, NoSuchAlgorithmException {
        // 计算MD5
        String fileMd5 = calculateMD5(file.getInputStream());

        // 查询MD5是否存在
        FileInfoDto existingFileInfo = fileInfoMapper.selectByFileMd5(fileMd5);
        if (existingFileInfo != null) {
            throw new BusinessException("您已经上传过该文件了,没必要重复上传");
        }

        // 初始化 FileInfoDto
        FileInfoDto fileInfoDto = createFileInfoDto(file, fileMd5);

        // 检查文件格式是否被允许
        if (!isAllowed(file.getOriginalFilename())) {
            throw new BusinessException("文件格式错误,请上传jpg, png, webp, jpeg, gif等图片格式");
        }

        // 构建文件存储路径
        String projectFolder = appConfig.getProjectFolder() + Constants.FILE_PATH;
        String datePath = LocalDate.now().format(DateTimeFormatter.ofPattern(DateTimePatternEnum.YYYY_MM.getPattern()));
        File uploadFileProjectFolder = new File(projectFolder + "/" + datePath);
        createDirectoryIfNotExists(uploadFileProjectFolder);

        // 构建文件夹路径
        String randomString1 = StringUtil.getRandomString(10);
        File fileFolder = new File(uploadFileProjectFolder.getPath() + "/" + randomString1);
        createDirectoryIfNotExists(fileFolder);

        // 构建新文件路径
        String fileName = file.getOriginalFilename();
        File newFile = new File(fileFolder.getPath() + "/" + fileName);

        // 设置文件信息 DTO
        setFileInfoDto(fileInfoDto, file, datePath, randomString1);

        // 保存文件信息
        fileInfoMapper.insert(fileInfoDto);

        // 保存文件和带水印的版本
        saveFileAndWatermark(file, newFile, fileFolder, fileName);

        // 返回文件信息 VO
        return createFileInfoVo(fileInfoDto, file);
    }


    private FileInfoDto createFileInfoDto(MultipartFile file, String fileMd5) {
        FileInfoDto fileInfoDto = new FileInfoDto();
        fileInfoDto.setFileMd5(fileMd5);
        fileInfoDto.setFileId(StringUtil.getRandomString(15));
        return fileInfoDto;
    }

    private void createDirectoryIfNotExists(File directory) {
        if (!directory.exists()) {
            directory.mkdirs();
        }
    }

    private void setFileInfoDto(FileInfoDto fileInfoDto, MultipartFile file, String datePath, String randomString1) {
        String fileName = file.getOriginalFilename();
        fileInfoDto.setFileName(fileName);
        fileInfoDto.setFileType(FileTypeEnum.getByCode(StringUtil.getFileSuffix(fileName)));
        fileInfoDto.setFileSize(file.getSize());
        fileInfoDto.setFilePath(Constants.FILE_PATH + "/" + datePath + "/" + randomString1 + "/" + fileName);
        fileInfoDto.setStatus(FileStatusEnum.ONE.getCode());
        fileInfoDto.setCreateTime(new Date());
    }

    private void saveFileAndWatermark(MultipartFile file, File newFile, File fileFolder, String fileName) throws IOException {
        // 保存上传文件到服务器
        file.transferTo(newFile);

        // 构建带水印的文件名
        String fileNameWithoutExtension = fileName.substring(0, fileName.lastIndexOf("."));
        String suffix = StringUtil.getSuffix(fileName);
        String watermarkName = fileNameWithoutExtension + "_" + suffix;
        File newWatermarkFile = new File(fileFolder.getPath() + "/" + watermarkName);

        // 为文件添加水印并压缩
        addWatermarkAndCompress(newFile, newWatermarkFile, "贺浩浩");
    }

    private FileInfoVo createFileInfoVo(FileInfoDto fileInfoDto, MultipartFile file) {
        FileInfoVo fileInfoVo = new FileInfoVo();
        fileInfoVo.setFileId(fileInfoDto.getFileId());
        fileInfoVo.setFileName(fileInfoDto.getFileName());
        fileInfoVo.setFilePath(fileInfoDto.getFilePath());
        fileInfoVo.setFileSize(file.getSize());
        fileInfoDto.setStatus(FileStatusEnum.ONE.getCode());
        return fileInfoVo;
    }
    /**
     * 计算输入流的MD5哈希值。
     *
     * 此方法通过读取输入流中的数据,然后使用MD5算法计算其哈希值。
     * MD5是一种广泛使用的加密散列函数,产生一个128位(16字节)的散列值。
     *
     * @param inputStream 要计算MD5哈希值的输入流。
     * @return 输入流数据的MD5哈希值的字符串表示。
     * @throws NoSuchAlgorithmException 如果MD5算法不可用。
     * @throws IOException 如果在读取输入流时发生错误。
     */
    private String calculateMD5(InputStream inputStream) throws NoSuchAlgorithmException, IOException {
        // 实例化MD5消息摘要算法
        MessageDigest md = MessageDigest.getInstance("MD5");

        // 创建一个缓冲区,用于从输入流中读取数据
        byte[] buffer = new byte[1024];
        int bytesRead;

        // 循环读取输入流中的数据,并更新消息摘要
        while ((bytesRead = inputStream.read(buffer)) != -1) {
            md.update(buffer, 0, bytesRead);
        }

        // 计算消息摘要
        byte[] digest = md.digest();

        // 将消息摘要转换为十六进制字符串
        StringBuilder sb = new StringBuilder();
        for (byte b : digest) {
            sb.append(String.format("%02x", b));
        }

        // 返回MD5哈希值的字符串表示
        return sb.toString();
    }



    /**
     * 检查文件名是否允许。
     *
     * 此方法通过检查文件名的扩展名来确定文件名是否被允许。文件名必须包含至少一个点(.)
     * 以标识扩展名,并且扩展名必须在预定义的允许扩展名列表中。
     *
     * @param fileName 要检查的文件名。
     * @return 如果文件名的扩展名被允许,则返回true;否则返回false。
     */
    private Boolean isAllowed(String fileName) {
        // 检查文件名是否为空或不包含点(.)
        if(fileName == null || fileName.lastIndexOf(".") == -1){
            return false;
        }
        // 提取文件名的扩展名,并转换为小写
        String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
        // 检查提取的扩展名是否在允许的扩展名列表中
        return ALLOWED_EXTENSIONS.contains(extension);
    }




    /**
     * 给图片添加水印并压缩保存。
     *
     * 此方法接收原始图片文件、目标输出文件和水印文本作为参数,它将原始图片读入,
     * 添加水印后,按照原始尺寸进行压缩,并保存到目标文件中。
     *
     * @param originalFile 原始图片文件,添加水印和压缩的基础文件。
     * @param outputFile 添加水印并压缩后的图片保存位置。
     * @param watermarkText 要添加的水印文本。
     * @throws IOException 如果读取或写入文件发生错误。
     */
    private void addWatermarkAndCompress(File originalFile, File outputFile,String watermarkText) throws IOException {
        // 读取原始图片
        BufferedImage originalImage = ImageIO.read(originalFile);
        // 添加水印
        BufferedImage watermarkedImage = addWatermark(originalImage, watermarkText);
        // 获取原图尺寸,以保持压缩后的图片尺寸与原图相同
        // 获取原图尺寸
        int originalWidth = originalImage.getWidth();
        int originalHeight = originalImage.getHeight();

        // 使用Thumbnails.of方法对添加水印后的图片进行缩放和压缩
        // 按比例缩放并压缩
        Thumbnails.of(watermarkedImage)
                .size(originalWidth, originalHeight)
                .outputQuality(0.4) // 调整画质压缩
                .toFile(outputFile);
    }


    /**
     * 给图片添加水印。
     *
     * @param image 原始图片。
     * @param watermarkText 水印文本。
     * @return 添加了水印的图片。
     */
    private BufferedImage addWatermark(BufferedImage image, String watermarkText) {
        // 获取原始图片的宽度和高度
        int imageWidth = image.getWidth();
        int imageHeight = image.getHeight();
        // 创建Graphics2D对象,用于在图片上绘制水印
        // 创建用于绘制水印的Graphics2D对象
        Graphics2D g2d = (Graphics2D) image.getGraphics();
        // 设置透明度,使水印呈现半透明效果
        // 设置水印的属性
        AlphaComposite alphaChannel = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f);
        g2d.setComposite(alphaChannel);
        // 设置水印文字颜色为灰色
        g2d.setColor(Color.GRAY);
        // 设置水印文字的字体、大小和样式
        // 使用支持中文的字体,例如SimHei(黑体)
        Font font = new Font("SimHei", Font.BOLD, 36);
        g2d.setFont(font);
        // 获取水印文字的尺寸信息
        FontMetrics fontMetrics = g2d.getFontMetrics();
        Rectangle2D rect = fontMetrics.getStringBounds(watermarkText, g2d);
        int textWidth = (int) rect.getWidth();
        int textHeight = (int) rect.getHeight();
        // 用于随机生成水印位置偏移量
        Random random = new Random();
        // 平铺方式添加水印,通过控制行间距和文字在行内的偏移,实现错落有致的布局效果
        // 平铺方式添加水印,单双行错开并随机偏移
        for (int y = 0; y < imageHeight; y += textHeight + 100) {
            // 判断当前行为偶数行还是奇数行,奇数行文字向右偏移
            boolean oddRow = (y / (textHeight + 100)) % 2 == 0;
            for (int x = oddRow ? 0 : textWidth / 2; x < imageWidth; x += textWidth + 300) {
                // 随机生成水平和垂直偏移量,使水印位置略有变化,避免整齐排列
                int xOffset = random.nextInt(100) - 50; // 随机偏移 -50 到 50 像素
                int yOffset = random.nextInt(50) - 25;  // 随机偏移 -25 到 25 像素
                // 在图片上绘制水印文字,位置略有偏移
                g2d.drawString(watermarkText, x + xOffset, y + yOffset);
            }
        }
        // 释放Graphics2D资源
        g2d.dispose();
        // 返回添加了水印的图片
        return image;
    }

    @Override
    public FileInfoVo getFileInfo(String fileId) {
        FileInfoVo fileInfoVo = fileInfoMapper.selectByFileId(fileId);
        if(null == fileInfoVo){
            throw new BusinessException("文件id错误,请检查之后重新请求");
        }
        if(fileInfoVo.getStatus().equals(FileStatusEnum.ZERO.getCode())){
            throw new BusinessException("文件已经被禁用了,无法查看");
        }
        return fileInfoVo;
    }

}

工具类

FileUtils

public class FileUtils {

    private static final Logger logger = LoggerFactory.getLogger(FileUtils.class);

    public static void readFile(HttpServletResponse response, String filePath) {
        // 如果文件路径不合法,则直接返回
        if (!pathIsOk(filePath)) {
            return;
        }

        OutputStream out = null;
        FileInputStream in = null;
        try {
            File file = new File(filePath);
            // 如果文件不存在,则直接返回
            if (!file.exists()) {
                return;
            }
            in = new FileInputStream(file);  // 打开文件输入流
            byte[] byteData = new byte[1024];  // 定义缓冲区
            out = response.getOutputStream();  // 获取响应输出流
            int len = 0;
            // 读取文件并写入响应输出流
            while ((len = in.read(byteData)) != -1) {
                out.write(byteData, 0, len);
            }
            out.flush();
        } catch (Exception e) {
            logger.error("读取文件异常", e);
        } finally {
            // 关闭输出流
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                    logger.error("IO异常", e);
                }
            }
            // 关闭输入流
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    logger.error("IO异常", e);
                }
            }
        }
    }

    public static boolean pathIsOk(String filePath) {
        if(isEmpty(filePath)){
            return true;
        }
        if(filePath.contains("../") || filePath.contains("..\\")){
            return false;
        }
        return true;
    }

    public static boolean isEmpty(String str) {
        if (null == str || "".equals(str) || "null".equals(str) || "\u0000".equals(str)) {
            return true;
        } else if ("".equals(str.trim())) {
            return true;
        }
        return false;
    }
}

StringUtil

public class StringUtil {
    public static String getSuffix(String fileName){
        String suffix = fileName.substring(fileName.lastIndexOf('.')).toLowerCase();
        return suffix;
    }

    public static String getFileSuffix(String fileName){
        String suffix = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
        return suffix;
    }

    public static final String getRandomString(Integer count){
        return RandomStringUtils.random(count,true,true);
    }
}

效果

 

 不带水印访问

 带水印访问

 

 总结

至于带水印我的实现是这样的,大家可以根据自己的思路来决定

比如没有登录访问的是水印版本,登录之后才能访问无水印版本

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/774176.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

第十四届蓝桥杯省赛C++B组F题【岛屿个数】题解(AC)

题目大意 给定一个 01 地图&#xff0c;分别表示陆地和海&#xff0c;问地图中一共有多少块岛屿&#xff1f;另外&#xff0c;若一个岛屿在另一个岛屿的内部&#xff0c;则不统计。如下图中的大岛屿包含着内部的小岛屿&#xff0c;故内部小岛屿不计算&#xff0c;最终输出 1。…

20W+喜爱的Pathview网页版 | 整合表达谱数据KEGG通路可视化

Pathview网站简介 网址&#xff1a;https://pathview.uncc.edu/ 前段时间介绍了一个R包 — Pathview。它可以整合表达谱数据并可视化KEGG通路&#xff0c;操作是先自动下载KEGG官网上的通路图&#xff0c;然后整合输入数据对通路图进行再次渲染。从而对KEGG通路图进行一定程度…

【Matlab 路径优化】基于蚁群算法的XX市旅游景点线路优化系统

基于蚁群算法的XX市旅游景点线路优化系统 &#xff08;一&#xff09;客户需求&#xff1a; ①考虑旅游景点的空间分布、游客偏好等因素&#xff0c;实现了旅游线路的智能规划 ②游客选择一景点出发经过所要游览的所有景点只一次&#xff0c;最后回到出发点的前提下&#xf…

【C++ 】解决 C++ 语言报错:Null Pointer Dereferenc

文章目录 引言 在 C 编程中&#xff0c;空指针解引用&#xff08;Null Pointer Dereference&#xff09;是一种常见且危险的错误。当程序试图通过空指针访问内存时&#xff0c;会导致程序崩溃或产生不可预期的行为。本文将详细探讨空指针解引用的成因、检测方法及其预防和解决…

首家!腾讯云数据万象通过中国信通院智能存储专项测试

2024年6月19日&#xff0c;由中国通信标准化协会主办&#xff0c;中国通信标准化协会大数据技术标准推进委员会(CCSA TC601)承办的首届“数据智能大会”在京隆重召开。腾讯云存储受邀出席了活动&#xff0c;大会中“可信数据智能”系列评估测试结果正式颁布&#xff0c;经过严苛…

JavaSE 面向对象程序设计进阶 Lambda表达式 2024年详解

Lambda表达式 作用 简化匿名内部类的书写 排序包装类数组 改写匿名内部类 代码实现 import java.util.Arrays; import java.util.Comparator;public class Main {public static void main(String[] args) {Integer[] arrnew Integer[]{2,1,3,4};Arrays.sort(arr,(Integer o1…

微信扫普通二维码打开小程序-详细实现

微信扫普通二维码链接打开小程序的官方文档地址&#xff1a;扫普通链接二维码打开小程序 | 微信开放文档 我们讲一下开发中的避坑点。 获取链接参数 本人项目采用UNIAPP&#xff0c;所以在开发的时候&#xff0c;牵扯打开页面的特殊性&#xff0c;在onLoad生命周期不执行。在…

公共事件应急日常管理系统-计算机毕业设计源码40054

公共事件应急日常管理系统的设计与实现 摘 要 本研究基于Spring Boot框架&#xff0c;设计并实现了公共事件应急日常管理系统&#xff0c;旨在提升公共事件的应急响应和日常管理效率。系统包括应急资源管理、物资申请管理、物资发放管理、应急培训管理、科普宣教管理、公共事件…

【数智化CIO展】中经社总工吴新丽:数字化是企业能力领域研究的深化和下探...

吴新丽 本文由中经社总工吴新丽投递并参与由数据猿联合上海大数据联盟共同推出的《2024中国数智化转型升级优秀CIO》榜单/奖项评选。 大数据产业创新服务媒体 ——聚焦数据 改变商业 当今时代&#xff0c;数字技术、数字经济是科技革命和产业变革的先机。数字经济发展速度之快…

Redis 多数据源 Spring Boot 实现

1.前言 本文为大家提供一个 redis 配置多数据源的实现方案&#xff0c;在实际项目中遇到&#xff0c;分享给大家。后续如果有时间会写一个升级版本&#xff0c;升级方向在第5点。 2.git 示例地址 git 仓库地址&#xff1a;https://github.com/huajiexiewenfeng/redis-multi-…

MAS马氏数控制榫机控制面板维修显示屏MDK3113B

马氏数控榫头机触摸屏/显示面板维修型号&#xff1a;MX3810A&#xff1b;MDK3113B&#xff1b;MXK2815B MAS马氏数控开榫机触摸屏/显示面板维修型号&#xff1a; MX2108B&#xff1b;MD2108A&#xff1b;MJ105А 数控面板维修包括&#xff1a;马氏数控榫头机、开榫机、制榫机…

视频共享融合赋能平台LnyonCVS国标视频监控平台包含哪些功能

随着国内视频监控应用的迅猛发展&#xff0c;系统接入规模不断扩大。不同平台提供商的接入协议各不相同&#xff0c;导致终端制造商在终端维护时需要针对不同平台的软件版本提供不同的维护&#xff0c;资源造成了极大的浪费。 为响应国家对重特大事件通过视频监控集中调阅来掌…

从0开始搭建Spring-Cloud微服务项目

文章目录 1. 安装Java开发环境配置环境变量 2. MySQL安装与配置环境变量配置配置MySQLNavicat配置Idea配置 1. 安装Java开发环境 安装Java开发环境主要涉及下载Java开发工具包&#xff08;JDK&#xff09;并配置环境变量&#xff0c;以便在系统中正确运行Java程序。 下载JDK …

onclick和@click有什么区别,究竟哪个更好使?

哈喽小伙伴们大家好,我是爱学英语的程序员,今天来给大家分享一些关于vue中事件绑定相关的内容,希望对大家有所帮助. 场景是这样的:我要实现一个切换栏,默认激活的是第一个标签,当鼠标移动到第二个标签是,对应的内容让激活.起初,我第一时间想到的是用element plus的组件来实现这…

从 Keycloak 导出和导入 Realm 和用户

1. 首先对keycloak 命令有所了解 需要将 Keycloak 中的 Realm 导出或导入时&#xff0c;您可以使用 JSON 文件进行操作。以下是一些有关导出和导入 Realm 的方法&#xff1a; 导出 Realm 到目录&#xff1a; 使用 export 命令将 Realm 导出到目录。在执行此命令时&#xff0c;…

QT 布局演示例子

效果 源码 #include <QApplication> #include <QWidget> #include <QSplitter> #include <QVBoxLayout> #include <QLabel>int main(int argc, char *argv[]) {QApplication app(argc, argv);QWidget mainWidget;mainWidget.setWindowTitle(&qu…

Jestson Orin Agx调试欧智通6162C-IC低功耗(BLE)蓝牙模块

一、准备工作 参考上一篇博客BLE低功耗蓝牙 二、使用蓝牙测试工具 gatttool 是 BlueZ 提供的一个工具&#xff0c;用于与 BLE 设备进行交互。 2.1&#xff1a;扫描设备并获取 MAC 地址 首先&#xff0c;你需要扫描你的 BLE 设备并获取其 MAC 地址。使用以下命令扫描设备&a…

数据融合工具(1)指定路径下同名图层合并

情景再现&#xff0c;呼叫小编 ————数据合并时&#xff0c;你是否也经常碰到这些情况&#xff1f; 数据存在几何错误&#xff0c;合并失败&#xff01; 数据字段类型不一致&#xff0c;合并失败&#xff01; 合并工具运行有警告信息&#xff0c;不知道是否合并成功&…

价值499的从Emlog主题模板PandaPRO移植到wordpress的主题

Panda PRO 主题&#xff0c;一款精致wordpress博客主题&#xff0c;令人惊叹的昼夜双版设计&#xff0c;精心打磨的一处处细节&#xff0c;一切从心出发&#xff0c;从零开始&#xff0c;只为让您的站点拥有速度与优雅兼具的极致体验。 从Emlog主题模板PandaPRO移植到wordpres…

GUKE万能工具箱(附带源码)

GUKE万能工具箱&#xff08;附带源码&#xff09; 效果图部分源码领取完整源码下期更新 效果图 部分源码 <!DOCTYPE html> <html><head><meta charset"utf-8" name"viewport" content"widthdevice-width, initial-scale1"…