news 2026/10/18 9:34:42

SpringBoot文件工具类实战:校验、存储与视频封面抽取

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot文件工具类实战:校验、存储与视频封面抽取

做一个西式点心的视频小程序时,最容易被低估的模块,往往不是推荐算法、不是 AI 对话,而是文件处理。视频上传、封面生成、头像裁剪、字幕导入、AI 分析素材暂存,每一个功能都绕不开同一个底层问题:文件怎么存、怎么校验、怎么命名、怎么清理。

很多项目做到后面出现磁盘被占满、文件名乱码、上传了非法格式、Linux 环境下路径拼接出错,问题根源都出在“文件工具类”这个不起眼的地基上。

本文是“基于 SpringBoot + Vue + Uniapp + LangChain4j + SpringAI 的智能视频推荐小程序”系列教程的第八讲,聚焦文件工具类的完整实现。前面几讲解决了项目骨架、小程序端框架、AI 对话与视频推荐链路,这一讲补上基础设施里最关键的一块拼图。

读完后,你会得到一套可以直接落地到 SpringBoot 项目中的文件处理工具类,包括扩展名白名单校验、大小限制、UUID 命名、日期分目录、视频封面抽取和定时清理策略。同时,我会把 LangChain4j 和 SpringAI 在文件场景中的关联位置讲清楚——AI 要理解一个视频,第一步永远是正确读取它的文件。

1. 智能视频推荐小程序为什么需要一个文件工具类

先说一个容易踩坑的事实:很多教程里所谓的“文件上传”,就是把MultipartFile.transferTo()一写,然后返回一个 URL。这种做法在本地 Demo 里看起来没问题,一旦进入真实项目,立刻暴露出三个问题。

第一个问题是文件内容不可控。没有任何校验,用户传了一个 5GB 的视频或者一个 .exe 文件,后端照单全收。存储空间被浪费,安全性也没有保障。

第二个问题是文件名不可控。用户上传的文件名可能包含中文、空格、特殊字符,如果直接拿原始文件名存到磁盘,轻则出现 URL 访问乱码,重则在 Linux 服务器上导致路径解析异常。

第三个问题是资源不可控。视频推荐小程序的特点是文件数量大、单文件体积大。如果没有按业务类型和日期分目录,没有统一的删除和清理机制,运维阶段会非常痛苦。

所以,这个项目的文件工具类不是简单封装几个static方法,而是要做四件事:

  • 校验:扩展名白名单 + 文件大小上限。
  • 存储:统一根目录,按业务类型和日期分目录,UUID 重命名。
  • 处理:提供视频封面抽取、临时文件清理等业务方法。
  • 对接:让 Controller、Service、AI 分析模块都能通过一套 API 操作文件,而不是各自为政。

这套设计做完,后续接 LangChain4j 做视频内容理解时,AI 模块只需要调用fileStorageService.getFile(fileId)拿到文件路径,再交给 LLM 处理即可,不需要关心文件是从哪儿来、存在哪里。

2. 文件工具类涉及的核心概念与边界

在开始写代码之前,先把几个容易混淆的概念理清。

2.1 本地存储、OSS 与 MinIO

本项目默认使用本地磁盘存储,也就是把文件保存在 SpringBoot 应用所在的服务器目录下。这样做的原因是:小程序开发阶段的用户量有限,本地存储足够支撑功能演示和联调。

如果后续上线,视频文件建议迁移到 MinIO 或 OSS。文件工具类的接口设计要预留切换空间,比如FileStorageService接口中只暴露save、load、delete三个方法,底层是本地磁盘还是对象存储,对调用方完全透明。

2.2 MultipartFile 与 File

SpringBoot 接收小程序端上传的文件时,参数类型是org.springframework.web.multipart.MultipartFile。它代表一个内存或临时磁盘中的上传文件,我们需要把它转存到指定目录。

这里有一个常见误区:很多人以为transferTo()之后就万事大吉。其实transferTo()对目标路径有要求,如果目标目录不存在,或者目标磁盘空间不足,会抛出 IOException。稳健的做法是:先创建目录,再执行保存。

2.3 扩展名校验白名单与 MIME 类型

扩展名判断是文件安全的第一道防线。常见的错误写法是直接判断filename.endsWith(".jpg"),这样遇到photo.jpg.exe就失效。更稳健的方式是取最后一个点后面的字符串,转成小写,再判断是否在白名单中。

MIME 类型同样可以被伪造,不建议作为唯一判断标准。本项目的做法是:扩展名白名单 + 文件大小限制 + 前端预览辅助校验,同时在后端解析视频文件时进行真实格式探测。

2.4 UUID 文件名与日期分目录

文件保存路径建议采用如下格式:

{basePath}/{bizType}/{yyyyMMdd}/{uuid}.{ext}

其中:

  • bizType:业务类型,比如avatar、video、cover。
  • yyyyMMdd:上传日期,避免单个目录文件过多,方便按日期清理。
  • uuid:唯一文件名,防止重名冲突,也避免用户原始文件名里的中文、特殊字符进入磁盘路径。

3. 环境准备与前置条件

本讲代码基于以下环境,版本请以你自己项目的实际版本为准,这里展示的是通用实现思路。

技术栈说明
JDK17+,项目使用 SpringBoot 3.x,所以 JDK 版本不能太低
Spring Boot3.x,集成 SpringAI 时需要使用与 Boot 3 匹配的版本
Maven负责依赖管理
MySQL业务数据存储
FFmpeg用于视频封面抽取,属于可选的本地命令行工具
Uniapp / Vue小程序端和后台管理端,本讲只涉及调用后端上传接口

在pom.xml中,除了 SpringBoot Web 依赖,还需要引入参数校验和文件存储配置所需的依赖。

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency>

spring-boot-configuration-processor用于让自定义配置类在 IDE 中获得提示,不是运行时的必须依赖,但在团队协作时体验更好。

如果你计划让 AI 模块直接读取视频文件做内容理解,建议同时确认spring-ai或langchain4j相关依赖已经加入。本讲不会展开 AI 调用细节,但会给出文件工具类如何为它们服务的示例。

4. 文件存储配置与统一配置类

首先在application.yml中增加文件存储配置。

file: storage: base-path: /data/video-recommend/files max-size: 104857600 allowed-extensions: - jpg - jpeg - png - mp4 - avi - mov cover: temp-path: /data/video-recommend/temp

配置项说明:

配置说明
file.storage.base-path文件上传根目录
file.storage.max-size单文件最大字节数,示例为 100MB
file.storage.allowed-extensions允许的扩展名列表
file.cover.temp-path视频封面抽取的临时文件目录

然后创建配置读取类。

// 文件路径:src/main/java/com/example/videorecommend/config/FileStorageProperties.java package com.example.videorecommend.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import java.util.List; @Component @ConfigurationProperties(prefix = "file.storage") public class FileStorageProperties { /** * 文件上传根目录 */ private String basePath; /** * 单文件最大字节数 */ private long maxSize; /** * 允许的扩展名 */ private List<String> allowedExtensions; public String getBasePath() { return basePath; } public void setBasePath(String basePath) { this.basePath = basePath; } public long getMaxSize() { return maxSize; } public void setMaxSize(long maxSize) { this.maxSize = maxSize; } public List<String> getAllowedExtensions() { return allowedExtensions; } public void setAllowedExtensions(List<String> allowedExtensions) { this.allowedExtensions = allowedExtensions; } }

使用@ConfigurationProperties而不是@Value的好处是:配置项多了之后代码更整洁,并且在 IDEA 中能看到配置提示。注意类中的属性名要和配置前缀匹配。

5. 文件工具类核心实现

这是本讲的重点。我会把文件工具类拆成三个部分:校验工具、存储服务、视频处理扩展。

5.1 扩展名与大小校验工具

先写一个纯静态工具类,负责最基本的校验逻辑。这个类可以脱离 Spring 环境测试,方便单元测试。

// 文件路径:src/main/java/com/example/videorecommend/util/FileValidateUtils.java package com.example.videorecommend.util; import com.example.videorecommend.common.exception.BusinessException; import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Locale; public class FileValidateUtils { private FileValidateUtils() { } /** * 获取文件扩展名,转小写 */ public static String getExtension(String filename) { if (filename == null || filename.lastIndexOf('.') < 0) { return ""; } return filename.substring(filename.lastIndexOf('.') + 1) .toLowerCase(Locale.ROOT); } /** * 校验扩展名是否在白名单中 */ public static boolean isAllowedExtension(String filename, List<String> allowed) { String ext = getExtension(filename); if (ext.isEmpty()) { return false; } return allowed.stream().anyMatch(ext::equalsIgnoreCase); } /** * 校验文件大小是否超过限制 */ public static boolean isAllowedSize(MultipartFile file, long maxSize) { return file.getSize() > 0 && file.getSize() <= maxSize; } /** * 综合校验,不通过时抛出业务异常 */ public static void validate(MultipartFile file, List<String> allowed, long maxSize) { if (file == null || file.isEmpty()) { throw new BusinessException("上传文件不能为空"); } if (!isAllowedExtension(file.getOriginalFilename(), allowed)) { throw new BusinessException("文件类型不支持,仅支持:" + allowed); } if (!isAllowedSize(file, maxSize)) { throw new BusinessException("文件大小超出限制,最大 " + maxSize + " 字节"); } } }

这里BusinessException是项目自定义异常,也可以替换成你自己的异常类,甚至直接使用IllegalArgumentException。关键在于:文件校验失败不要静默返回,而要抛出清晰异常,由全局异常处理器统一转成 JSON 响应给前端。

5.2 文件存储服务

下一层是FileStorageService。它负责真正的磁盘读写、路径规划和删除操作。

为什么不能把保存逻辑也放在静态工具类里?因为保存需要读取配置(根目录、允许扩展名),静态方法不方便注入配置,也难以在测试中替换。所以项目里更推荐:纯逻辑放静态工具类,涉及 IO 和配置放 Spring Service。

// 文件路径:src/main/java/com/example/videorecommend/service/FileStorageService.java package com.example.videorecommend.service; import com.example.videorecommend.config.FileStorageProperties; import com.example.videorecommend.util.FileValidateUtils; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.UUID; @Service public class FileStorageService { private final FileStorageProperties fileStorageProperties; public FileStorageService(FileStorageProperties fileStorageProperties) { this.fileStorageProperties = fileStorageProperties; } /** * 保存文件,返回相对路径 * * @param file 上传文件 * @param bizType 业务类型,比如 avatar / video / cover */ public String saveFile(MultipartFile file, String bizType) { FileValidateUtils.validate( file, fileStorageProperties.getAllowedExtensions(), fileStorageProperties.getMaxSize() ); String ext = FileValidateUtils.getExtension(file.getOriginalFilename()); String datePath = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String uuid = UUID.randomUUID().toString().replace("-", ""); String filename = uuid + "." + ext; // 相对路径:bizType/yyyyMMdd/uuid.ext String relativePath = bizType + "/" + datePath + "/" + filename; Path targetDir = Paths.get(fileStorageProperties.getBasePath(), bizType, datePath); Path targetFile = Paths.get(fileStorageProperties.getBasePath(), relativePath); try { Files.createDirectories(targetDir); file.transferTo(targetFile); } catch (IOException e) { throw new RuntimeException("文件保存失败", e); } return relativePath; } /** * 根据相对路径删除文件 */ public boolean deleteFile(String relativePath) { if (relativePath == null || relativePath.isBlank()) { return false; } Path filePath = Paths.get(fileStorageProperties.getBasePath(), relativePath); try { return Files.deleteIfExists(filePath); } catch (IOException e) { return false; } } /** * 获取文件的完整访问 URL(本地静态资源映射后的地址) */ public String buildFileUrl(String relativePath) { return "/files/" + relativePath; } }

关键点:

  • DateTimeFormatter.BASIC_ISO_DATE会生成20250601格式的日期目录,比带横杠的格式更适合目录命名。
  • UUID.randomUUID().toString().replace("-", "")生成 32 位无横杠字符串,避免文件名过长。
  • file.transferTo(targetFile)在targetDir创建完成后调用,确保目标目录存在。
  • 返回值是相对路径,这样未来切换 OSS 时,buildFileUrl只需改成拼接 OSS 域名,业务层不用大改。

5.3 静态资源映射

为了让上传后的文件可以通过 URL 访问,需要配置 SpringBoot 的静态资源映射,把/files/**映射到本地根目录。

// 文件路径:src/main/java/com/example/videorecommend/config/WebMvcConfig.java package com.example.videorecommend.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebMvcConfig implements WebMvcConfigurer { private final FileStorageProperties fileStorageProperties; public WebMvcConfig(FileStorageProperties fileStorageProperties) { this.fileStorageProperties = fileStorageProperties; } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String basePath = fileStorageProperties.getBasePath(); String location = basePath.endsWith("/") ? "file:///" + basePath : "file:///" + basePath + "/"; registry.addResourceHandler("/files/**") .addResourceLocations(location); } }

这里有一点需要特别注意:Windows 和 Linux 的路径写法不同。Windows 下file:///D:/data/video-recommend/files/,Linux 下file:///data/video-recommend/files/。项目上线后往往部署在 Linux,所以不要把D:这类路径写死在代码里,一定要从配置读取。

6. 视频封面抽取与 AI 模块的衔接

智能视频推荐小程序里,视频封面是推荐流展示的关键。用户上传视频后,系统需要自动抽取一帧作为封面。这个动作本质上也是一类文件操作,值得纳入文件工具类的扩展能力。

本项目的实现方式是:调用本地 FFmpeg 命令,从视频第 1 秒抽取一帧画面,生成封面图片。

// 文件路径:src/main/java/com/example/videorecommend/util/VideoProcessUtils.java package com.example.videorecommend.util; import java.io.IOException; import java.nio.file.Path; public class VideoProcessUtils { private VideoProcessUtils() { } /** * 使用 FFmpeg 抽取视频指定时间的某一帧 * * @param videoPath 视频完整路径 * @param coverPath 输出的封面图片路径 * @param time 抽取时间点,格式如 "00:00:01" */ public static boolean extractFrame(String videoPath, String coverPath, String time) { ProcessBuilder processBuilder = new ProcessBuilder( "ffmpeg", "-i", videoPath, "-ss", time, "-vframes", "1", "-y", coverPath ); processBuilder.redirectErrorStream(true); try { Process process = processBuilder.start(); int exitCode = process.waitFor(); return exitCode == 0; } catch (IOException | InterruptedException e) { Thread.currentThread().interrupt(); return false; } } }

在FileStorageService中增加一个组合方法:保存视频后,立即抽取封面,并把封面文件也纳入统一存储路径。

public String saveVideoAndGenerateCover(MultipartFile videoFile) { // 1. 保存视频 String videoRelativePath = saveFile(videoFile, "video"); // 2. 构造完整路径 Path videoFullPath = Paths.get(fileStorageProperties.getBasePath(), videoRelativePath); // 3. 生成封面相对路径 String ext = "jpg"; String datePath = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE); String uuid = UUID.randomUUID().toString().replace("-", ""); String coverRelativePath = "cover/" + datePath + "/" + uuid + "." + ext; Path coverFullPath = Paths.get(fileStorageProperties.getBasePath(), coverRelativePath); Path coverDir = coverFullPath.getParent(); try { if (coverDir != null) { Files.createDirectories(coverDir); } boolean success = VideoProcessUtils.extractFrame( videoFullPath.toString(), coverFullPath.toString(), "00:00:01" ); if (success) { return coverRelativePath; } } catch (IOException e) { throw new RuntimeException("视频封面生成失败", e); } // 4. 抽取失败时返回 null,由业务层决定后续逻辑 return null; }

这段代码把“上传视频 + 生成封面”组合成了一个完整业务动作,数据库里可以同时保存视频路径和封面路径。

接下来回到 AI 模块。LangChain4j 和 SpringAI 要做视频内容理解或者给视频生成推荐标签,首先需要一个可读的输入来源。本项目目前的做法是:先把视频抽帧成图片,再把图片作为多模态输入交给 AI 模型。也就是说,文件工具类抽出的封面帧,同时也是 AI 视觉模型的输入素材。

// 伪代码示例:AI 模块读取封面,交给 LangChain4j 做图像理解 String coverFullPath = "/data/video-recommend/files/" + coverRelativePath; ImageUrl imageUrl = ImageUrl.builder() .url(coverFullPath) .build(); String description = langChainService.describeImage(imageUrl);

实际接入时,你需要根据所用模型支持的输入格式调整。核心思路是:不要让 AI 模块直接操作磁盘文件,而是由文件工具类统一产出图片、文本或字幕文件,AI 模块只消费这些产出物。这样文件路径变化、存储切换都不会影响 AI 调用代码。

7. 控制器层与前端上传接口对接

工具类最终要通过 Controller 暴露给前端小程序调用。

// 文件路径:src/main/java/com/example/videorecommend/controller/FileController.java package com.example.videorecommend.controller; import com.example.videorecommend.common.ApiResponse; import com.example.videorecommend.service.FileStorageService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.multipart.MultipartFile; import java.util.HashMap; import java.util.Map; @RestController @RequestMapping("/api/file") public class FileController { private final FileStorageService fileStorageService; public FileController(FileStorageService fileStorageService) { this.fileStorageService = fileStorageService; } /** * 上传视频并生成封面 */ @PostMapping("/upload/video") public ApiResponse<Map<String, Object>> uploadVideo(@RequestParam("file") MultipartFile file) { String videoPath = fileStorageService.saveFile(file, "video"); String coverPath = fileStorageService.saveVideoAndGenerateCover(file); Map<String, Object> data = new HashMap<>(); data.put("videoUrl", fileStorageService.buildFileUrl(videoPath)); data.put("coverUrl", coverPath == null ? "" : fileStorageService.buildFileUrl(coverPath)); return ApiResponse.success(data); } /** * 上传头像 */ @PostMapping("/upload/avatar") public ApiResponse<String> uploadAvatar(@RequestParam("file") MultipartFile file) { String avatarPath = fileStorageService.saveFile(file, "avatar"); return ApiResponse.success(fileStorageService.buildFileUrl(avatarPath)); } }

前端 Uniapp 小程序端通过uni.uploadFile上传文件,代码大致如下:

uni.chooseVideo({ count: 1, sourceType: ['album', 'camera'], success: (res) => { uni.uploadFile({ url: 'https://你的后端域名/api/file/upload/video', filePath: res.tempFilePath, name: 'file', success: (uploadRes) => { const result = JSON.parse(uploadRes.data); console.log('视频地址:', result.data.videoUrl); console.log('封面地址:', result.data.coverUrl); } }); } });

注意name字段必须和后端@RequestParam("file")的参数名保持一致,否则后端接收不到文件。

Vue 管理后台中用el-upload或者普通input[type=file]上传,逻辑类似,只需要把FormData中的 key 也设为file。

8. 运行结果与验证

启动 SpringBoot 项目后,用 Postman 或者 curl 模拟一次文件上传。

curl -X POST http://localhost:8080/api/file/upload/avatar \ -F "file=@/Users/test/avatar.jpg"

预期返回:

{ "code": 200, "message": "success", "data": "/files/avatar/20250601/3f1a9b0c2d6e4f5a8b7c9d0e1f2a3b4c.jpg" }

在浏览器中访问http://localhost:8080/files/avatar/20250601/3f1a9b0c2d6e4f5a8b7c9d0e1f2a3b4c.jpg,如果能看到图片,说明静态资源映射和文件保存都正常。

验证目录结构:

/data/video-recommend/files └── avatar └── 20250601 ├── 3f1a9b0c2d6e4f5a8b7c9d0e1f2a3b4c.jpg └── 9e7a1b2c3d4e5f6a7b8c9d0e1f2a3b4c.jpg

如果上传了一个超出允许扩展名的文件,响应中应当返回异常信息,例如:

{ "code": 400, "message": "文件类型不支持,仅支持:[jpg, jpeg, png, mp4, avi, mov]", "data": null }

当视频上传成功但封面生成失败时,先确认服务器上是否安装 FFmpeg,并且ffmpeg命令是否在 PATH 中。在 Linux 上可以用which ffmpeg检查。

9. 常见问题与排查思路

问题现象可能原因排查方式解决方案
上传文件后访问 URL 返回 404静态资源映射路径未生效查看 WebMvcConfig 是否被扫描到确认 config 包在 SpringBoot 启动类能扫描到的位置
文件名变成乱码原始文件名包含中文或特殊字符检查数据库和磁盘文件名统一使用 UUID 重命名,不要保留原始文件名
transferTo 报 NoSuchFileException目标目录不存在检查 basePath 目录是否创建保存前先 Files.createDirectories
文件上传成功后没有权限访问Linux 目录权限不足检查 Nginx 或应用进程的运行用户使用 chmod 或为上传目录分配正确权限
视频封面生成失败FFmpeg 未安装或视频编码不兼容手工执行 ffmpeg 命令看报错安装 FFmpeg,或改用 JavaCV 方案
上传大视频时请求超时默认的 SpringBoot 上传大小限制过低查看spring.servlet.multipart.max-file-size在 application.yml 中调大限制
磁盘空间被占满文件没有定时清理检查临时文件和 deleted 记录启动定时任务,定期清理过期文件

针对最后一条,我在项目中增加了一个简单的定时清理任务,每天凌晨清理临时目录中超过 7 天的文件。这是文件工具类保持生产可用的关键一环,否则测试环境会积累大量垃圾文件。

// 文件路径:src/main/java/com/example/videorecommend/task/FileCleanTask.java package com.example.videorecommend.task; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDate; import java.time.temporal.ChronoUnit; @Component public class FileCleanTask { private static final Logger log = LoggerFactory.getLogger(FileCleanTask.class); /** * 每天凌晨 2 点执行一次 */ @Scheduled(cron = "0 0 2 * * ?") public void cleanTempFiles() { Path tempRoot = Paths.get("/data/video-recommend/temp"); try (var stream = Files.walk(tempRoot, 3)) { stream.filter(Files::isRegularFile) .filter(path -> { try { LocalDate fileDate = LocalDate.parse( path.getParent().getFileName().toString() ); return ChronoUnit.DAYS.between(fileDate, LocalDate.now()) > 7; } catch (Exception e) { return true; } }) .forEach(path -> { try { Files.deleteIfExists(path); log.info("清理临时文件:{}", path); } catch (IOException e) { log.warn("清理临时文件失败:{}", path, e); } }); } catch (IOException e) { log.error("扫描临时目录失败", e); } } }

注意,定时任务类需要配合@EnableScheduling才能生效。可以在启动类上添加该注解。简单的实现是:遍历临时目录,根据日期目录名判断过期时间,删除超过 7 天的文件。这个任务仅清理临时目录,不能清理正式上传目录,避免误删用户数据。

10. 最佳实践与工程建议

文件工具类写完后,项目中的使用体验会明显改善。经过这段代码落地,以下几条建议非常值得保留到团队规范中。

第一,所有文件访问都通过 Service 层,禁止在业务代码里直接拼接磁盘路径。典型的坏味道是 Controller 里出现Paths.get("/data/xxx/" + userId + "/" + filename)。这种代码一旦出现两次,路径规则就失控了。统一收敛到FileStorageService,后续加权限、加缓存、换 OSS 都容易很多。

第二,保存文件前先做扩展名校验,但也不要完全相信扩展名。视频推荐小程序的上传入口可能被脚本攻击,白名单只能挡住一半。更稳妥的做法是保存后用 FFmpeg 或 JavaCV 做真实格式探测,确认文件确实是视频,再写入数据库。这样会多花一点资源,但安全性值得。

第三,数据库只存相对路径,不存绝对路径。如果数据库里存的是/data/video-recommend/files/avatar/20250601/uuid.jpg,未来迁移到对象存储或更换应用服务器,所有历史数据都要更新。如果只存avatar/20250601/uuid.jpg,加一层buildFileUrl做拼接,迁移成本可以忽略。

第四,给不同业务类型的文件设置不同的目录,并规划好生命周期。头像、封面这类小文件保留时间长;视频原文件体积大,可能要考虑转码后删除原文件;临时素材文件则定期清理。目录设计越早规划,后续运维越省心。

第五,应对外部访问权限。视频推荐小程序的封面和头像通常是公开访问的,但部分上传文件可能是用户私密内容。在 Controller 层做访问控制,或者把私有文件放在静态资源映射之外,通过认证接口下载。本项目目前对封面、头像使用公开 URL,私密文件不在本讲范围内。

第六,注意上传大小限制。SpringBoot 默认上传大小是 1MB,对于视频项目来说远远不够,一定要在application.yml中调整。

spring: servlet: multipart: max-file-size: 200MB max-request-size: 220MB

max-file-size是单文件限制,max-request-size是单次请求的总大小。如果前端一次上传多个文件,两个参数都要合理设置。

11. 总结与下一步实践建议

本讲的篇幅虽然集中在“文件工具类”,但它解决的是智能视频推荐小程序从开发到上线整个链路中非常基础的问题。回到开头那个判断:文件处理模块写得好的项目,和写得稀烂的项目,差距在前几周还不明显,一旦开始联调、测试、部署,稳定性的差距会瞬间拉开。

这一讲真正讲清了几件事:

  • 文件工具类的定位不是“上传工具”,而是统一管理校验、存储、路径、清理、AI 素材产出的基础设施。
  • 校验逻辑和存储逻辑分离,纯静态工具类负责判断,Spring Service 负责 IO 和配置。
  • UUID 重命名 + 日期分目录 + 相对路径存储,是本地存储阶段最稳妥的组合方式。
  • 视频封面抽取通过 FFmpeg 命令实现,抽取结果同时也是 LangChain4j / SpringAI 多模态输入的一部分。
  • 定时清理临时文件是生产环境必不可少的补充机制。

下一步建议你做三件事:

  1. 把上面的FileStorageService、VideoProcessUtils和FileValidateUtils复制到自己的项目中,根据实际情况调整BusinessException类型和配置项。
  2. 配置一个测试用的上传接口,用 curl 上传一张图片、一段小视频,确认磁盘目录、URL 访问、异常提示都符合预期。
  3. 如果你的项目已经接入了 LangChain4j 或 SpringAI,试着把生成的封面图片作为多模态输入传给模型,验证整个“文件 -> 图片 -> AI 理解”的链路能否跑通。

下一讲可以继续围绕视频推荐的核心流程展开,比如视频标签生成、用户向量检索或者推荐列表的组装。每一步都会用到文件工具类产出的素材。先把文件地基打牢,后面的功能才走得稳。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 9:34:09

MagicDraw 15.0 安装部署与离线激活配置指南

简介&#xff1a;UML建模工具是软件工程中承载架构设计的重要载体&#xff0c;而基于JVM运行的桌面建模环境&#xff0c;其稳定性高度依赖运行时版本匹配与授权配置。在企业内网隔离、历史模型库维护或旧版工程兼容等场景中&#xff0c;选对与目标工程匹配的软件版本&#xff0…

作者头像 李华
网站建设 2026/10/18 9:34:04

基于Spring Boot的大学生创新成果信息管理系统设计与实现

每年的毕业设计选题季&#xff0c;总有人来问&#xff1a;做什么题目既能让答辩老师觉得工作量够&#xff0c;又不会把自己埋进一个太大的坑里&#xff1f;我自己的经验是&#xff0c;选一个业务场景具体、管理链路清晰、技术落点明确的题目&#xff0c;比追热点稳得多。大学生…

作者头像 李华
网站建设 2026/10/18 9:28:34

软件评测师备考:每日一练错题集揭示测试设计与流程高频误区

1. 为什么我坚持用错题集备考评测师&#xff1a;从丢分习惯看复习重点每天刷完题&#xff0c;我最大的感受是&#xff1a;软考评测科目的丢分&#xff0c;大多不是不会做&#xff0c;而是“以为自己会了”。比如一道题目&#xff0c;四个选项里有两个长得非常像&#xff0c;都是…

作者头像 李华
网站建设 2026/10/18 9:27:56

负载均衡式在线评测系统高并发架构全解析

负载均衡式在线评测系统&#xff08;Load-Balanced Online OJ&#xff09;技术全景指南做在线评测系统&#xff08;Online Judge&#xff0c;简称 OJ&#xff09;这事儿&#xff0c;说难不难&#xff0c;说简单也真不简单。尤其是当你的用户从十几个同班同学变成几百人同时刷题…

作者头像 李华