☰
SpringBoot+Vue+MyBatis多媒体素材管理系统前后端分离实战
2026/9/30 11:11:03 网站建设 项目流程

在多媒体生产、线上培训、设计协作这类业务里,素材两字最容易被低估。图片散落在聊天记录和各个网盘链接里,视频存在同事的移动硬盘上,想找某个版本的设计稿得问遍整个部门。我最近完整落地了一套前后端分离的多媒体素材管理系统,技术栈就是大家最熟悉的SpringBoot + Vue + MyBatis + MySQL这套组合,从后端接口、数据库脚本到前端页面、部署配置全部打通。这篇文章会把整个项目从设计到上线拆开揉碎了讲,包括表结构怎么建、分页查询怎么写、文件上传怎么接、Nginx怎么配,以及我实际踩过的各种坑。正在做毕业设计、准备晋升答辩、或者想快速搭一套内部素材管理平台的朋友,可以直接照虎画猫。

1. 项目定位与整体架构拆解

1.1 为什么偏偏是这套技术栈组合

SpringBoot + Vue + MyBatis + MySQL在Java Web开发里的地位有点像家常菜里的番茄炒蛋,朴实无华但万能百搭。选择这套方案不是因为追新,是因为它把"快速落地"和"长期可维护"这两个看似矛盾的需求平衡得最好。SpringBoot解决了Java后端项目配置繁琐、起步慢的老问题,内嵌的Tomcat让部署变成一个java -jar命令的事;Vue在前端把页面拆成组件,素材卡片、上传弹窗、预览抽屉都是独立零件,改一处不炸全局;MyBatis把SQL写在XML里,上线后做SQL调优、加索引、改执行计划都有明确抓手;MySQL则是那个最稳的底座,事务、索引、权限都成熟到几乎不用操心。

对比过其他方案才能说清楚为什么选这套:用Spring Cloud做单机项目就是杀鸡用牛刀,几十个依赖打进来启动慢不说,排查问题还要绕好几层;用JPA取代MyBatis,虽然写代码快,但复杂统计查询时生成的SQL经常不听话,一旦数据量上来,调优难度直线上升;前端不用Vue用React,本质上没多大差距,但对国内大多数团队来说,Vue的入门曲线更友好,中文生态也完善,遇到问题能找到的现成经验更多。这套组合不追求技术上的标新立异,胜在从招人到维护,从开发到部署,每个环节都有成熟的案例能参考。

1.2 前后端分离在素材管理场景里的实际收益

前后端分离,拆开的其实是两拨人、两套代码、两个部署单元。我见过不少团队把前端页面直接扔在SpringBoot的static目录里,表面省事,实际上后续每一次前端改动都要重新打一次后端包,版本管理是一笔糊涂账。分离之后,前端工程独立维护,后端只提供一套JSON接口,前端调用接口拿数据、渲染页面,两者之间唯一约定就是接口文档。

在素材管理系统这个场景里,前后端分离还有一层特别实际的收益:多媒体素材的类型多元化意味着前端展示形态非常多。图片要能按卡片墙预览,视频要能内嵌播放器,PDF要能调起在线阅读器,如果用模板引擎在后端渲染,每加一种素材类型就要改一次后端页面模板,而后端开发改页面本身就效率低下。Vue把不同素材类型的展示逻辑封装成独立组件,加一种格式就是加一个vue文件的事,后端接口一个字都不用动。项目上线部署时,前端构建产物是一个纯静态的dist目录,扔给Nginx托管就行,后端jar包可以部署在独立服务器,两个人互不干扰,哪部分出问题就单独排查哪部分。

2. 数据库模型与MyBatis层实现

2.1 多媒体素材系统的核心表该怎么设计

我设计数据库的习惯是先把业务对象画出来,再考虑字段和关系。这套系统的核心业务对象就是素材,围绕素材展开的有分类、有标签、有上传者、有操作记录。表结构设计上有一个重要权衡要提前想清楚:素材的元数据信息存MySQL,素材的文件本体不直接进数据库。文件本体放在MinIO对象存储或本地磁盘,数据库里只挂文件路径的URL。数据库是关系型事务型的,存大文件会拖垮查询性能,而文件存储天然适合OSS这类存储服务。

素材主表设计上,我把常用字段列出来:

CREATE TABLE `material_info` ( `id` bigint NOT NULL AUTO_INCREMENT, `material_name` varchar(255) NOT NULL COMMENT '素材名称', `material_type` varchar(20) NOT NULL COMMENT '素材类型: IMAGE/VIDEO/AUDIO/DOC', `file_size` bigint DEFAULT NULL COMMENT '文件大小(字节)', `file_url` varchar(500) DEFAULT NULL COMMENT '文件访问路径', `cover_url` varchar(500) DEFAULT NULL COMMENT '封面图路径,视频和文档预览用', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `tag_name` varchar(255) DEFAULT NULL COMMENT '标签,逗号分隔', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态: 0禁用 1正常', `download_count` int NOT NULL DEFAULT '0' COMMENT '下载次数', `create_by` varchar(64) DEFAULT NULL COMMENT '上传人', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '上传时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_type_category` (`material_type`, `category_id`), KEY `idx_create_time` (`create_time`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多媒体素材信息表';

这张表设计里有几个细节值得琢磨一下。material_type用字符串枚举而不是用数字枚举,原因很简单:代码可读性优先,看到IMAGE比看到1直观得多,查询时where material_type = 'IMAGE'也足够高效,加上前缀索引后几乎无损耗。material_name字段单独存一份而不是每次去文件存储服务查,就是为了列表页展示时不需要逐个请求对象存储,解耦了展示层和文件层。下载计数字段是预留的统计能力,后台看哪些素材是热门资源,这个字段配合定时任务刷数据比临时count聚合省太多压力。

多表设计上,我没有单独建素材标签表,而是在素材表里用了tag_name字段存逗号分隔的标签字符串。这是刻意做的反范式设计。素材系统的标签查询是所有高级查询里最边缘的场景,单独建一张关联表虽然符合第三范式,但每次查询都要JOIN两张表、聚合标签字符串,性能开销大,代码复杂度也高。核心的查询路径永远是按分类筛选、按名称模糊搜索、按时段过滤,标签只是锦上添花的补充维度。反规范化设计用空间和一致性的极小代价,换来查询性能和代码可读性的大幅提升,这笔账是划算的。

2.2 MyBatis动态SQL与分页查询的工程落地

MyBatis的XML里头最核心的能力就是动态SQL。素材管理系统的列表页是整个系统访问最频繁的接口,它的查询条件天然是不固定的:用户可能只按类型筛,可能又加上一个分类,可能还要按素材名称关键词搜,如果为每种组合都写一条独立SQL,那Mapper接口的维护成本会失控。动态SQL的写法是把这个复杂度直接在XML里化解掉。

分页处理上,这套项目选用了PageHelper插件。它实现分页的原理是在MyBatis执行SQL之前通过拦截器把原始SQL包装成带LIMIT的查询语句,同时发一条COUNT查询取总条数。使用上有个大坑要特别注意:PageHelper分页紧跟其后的第一条SQL查询才会被拦截分页,也就是说一旦PageHelper.startPage()和Mapper查询之间夹了任何其他查询语句,分页就不会生效,甚至会产生数据错乱。我在项目里要求所有分页查询都必须遵守"startPage后立即调用Mapper方法"的纪律,并且封装了一个统一的分页返回结果,避免别人在不了解原理的情况下乱插代码。

<select id="selectMaterialPage" resultType="com.example.entity.MaterialInfo"> SELECT * FROM material_info <where> <if test="type != null and type != ''"> AND material_type = #{type} </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND material_name LIKE CONCAT('%', #{keyword}, '%') </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY create_time DESC </select>

这段动态SQL有三个容易踩的细节。第一个是WHERE和AND的处理,用 标签而不是直接拼WHERE,MyBatis会自动判断如果if条件一个都没命中,就把整个WHERE关键字去掉;如果有条件命中,它会帮我们把第一个连词AND自动剥掉,这是java开发里最经典的细节。第二个是模糊查询里CONCAT('%', #{keyword}, '%')的写法,如果直接写'%${keyword}%',虽然也能跑,但这属于SQL注入的高危写法,能用#{}绑定的参数就绝不碰${}。第三个是#{}和${}的本质区别,#{}走的是PreparedStatement占位符,${}是字符串直接拼接进SQL,模糊查询用#{}配CONCAT,既安全又灵活。

3. SpringBoot后端:从接口设计到文件流处理

3.1 分层架构与RESTful接口设计规范

后端工程按Controller、Service、Mapper三层来切,这是SpringBoot项目最经典的划分方式。Controller只做参数接收、调用Service、返回结果这三件事,不写任何业务逻辑;Service层承载具体业务,比如上传素材时要校验文件类型、生成缩略图、落库记录;Mapper层是数据访问的门面。这样一个请求的链路是:前端Ajax请求 → SpringMVC分发到Controller → Controller调Service → Service调Mapper → Mapper执行SQL操作MySQL → 结果逐层返回,最后以JSON格式响应给前端。

接口设计遵循RESTful风格,但我不做极端原教旨主义。很多文章会严格规定POST和PUT的语义边界,实际团队协作里更现实的做法是接口名直接把动作写在URL路径上,反而比四五个HTTP方法区分得更清晰。这套项目里的核心接口如下:

请求方式接口路径功能说明
POST/api/material/upload上传多媒体素材文件
GET/api/material/page分页查询素材列表
GET/api/material/detail/{id}获取素材详情
PUT/api/material/update更新素材信息(重命名/改分类)
DELETE/api/material/delete/{id}删除指定素材
GET/api/material/download/{id}下载素材文件
GET/api/category/tree获取分类树

每个接口返回值我都统一封装成一个Result对象,里面包含code、message、data三个字段。这个习惯非常重要,它带来的直接好处是前端axios拦截器只需要判断code是否为200决定走成功分支还是错误分支,不需要在每个页面里分别处理各种异常形态。项目后期加全局异常处理器之后,Service层抛出的业务异常会自动被转换成统一的错误JSON,前端代码一点不用动,这就是接口契约化设计的价值。

3.2 文件上传与对象存储的完整链路

素材系统里最核心的功能就是上传。文件上传的链路是:前端把文件通过multipart/form-data格式发送到后端接口,SpringMVC的MultipartFile对象接住文件流,接下来后端要把这个文件流存到一个文件存储服务里,再拿到一个可访问的URL地址,最后把URL和素材元数据一起写进MySQL。文件存储的选型,我在项目里强烈建议接MinIO而不是直接把文件写到本地磁盘。直接写本地磁盘有个很隐蔽的坑:应用重新部署时,如果磁盘路径的配置变了,老文件的访问地址就全部失效了;多实例部署时,文件分散在各个实例的本地磁盘,根本无法统一管理。MinIO作为对象存储服务,提供独立的访问端点,应用只管往里面put对象拿URL,存储空间和服务实例完全解耦,后面扩容、迁移都是存储层自己的事。

集成MinIO其实比很多人想的简单,SpringBoot里引入MinIO的Java SDK,配置好endpoint、accessKey、secretKey,写一个配置类把MinioClient注册成Bean,之后在Service里就能直接注入使用了。上传代码的核心逻辑大概是:

public String uploadFile(MultipartFile file, String objectName) { try { // 检查bucket是否存在,不存在则创建 boolean bucketExists = minioClient.bucketExists(BucketExistsArgs.builder() .bucket(bucketName).build()); if (!bucketExists) { minioClient.makeBucket(MakeBucketArgs.builder() .bucket(bucketName).build()); } // 上传文件流 minioClient.putObject(PutObjectArgs.builder() .bucket(bucketName) .object(objectName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); // 拼接访问URL return minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() .bucket(bucketName) .object(objectName) .method(Method.GET) .expiry(7 * 24 * 3600) .build()); } catch (Exception e) { log.error("文件上传MinIO失败", e); throw new BusinessException("文件上传失败"); } }

这段代码里有个参数值得展开:stream(file.getInputStream(), file.getSize(), -1),这个方法签名是stream输入流、对象大小、分片大小。第三个参数-1表示不指定分片大小,让SDK自动判断。通过getPresignedObjectUrl生成的URL是带签名的临时访问链接,有效时长我设置为7天。有人问为什么不用公开读权限的bucket直接拼URL,因为带签名的URL能防止别人拿到地址后长期盗用资源,内部系统用这个安全策略足够了。

上传后还有个重要环节是缩略图和封面处理。图片类素材,后端接收到之后用Thumbnailator库生成一张压缩图;视频类素材,用FFmpeg抽取一帧画面作为封面图存到MinIO。这些操作属于IO密集型和计算密集型任务,如果放在上传请求里同步执行,用户等一个几百MB的视频上传完还要再等好几秒的封面抽取,体验会很差。我在这里的处理方案是引入Spring的@Async异步方法,上传请求只负责同步写入文件数据和数据库记录,封面生成、缩略图生成全部丢到异步线程池里执行,前端接口立刻返回"上传成功",等封面就绪后通过WebSocket或前端轮询更新缩略图展示。

3.3 安全校验和全局异常处理需要注意的细节

接口安全是个不能省略的环节。这套系统里我做了两层防护:第一层是Spring Security + JWT实现的登录鉴权,用户登录成功后拿到token,之后的每次请求在请求头里带上token,后端通过拦截器校验token的有效性。这样未登录的用户无法访问任何素材接口,有效保护了素材资源的私密性。第二层是参数校验,SpringBoot的@Validated注解配合实体类上的@NotNull、@Size注解,在Controller层就把非法请求拦截下来,省得非法的分类ID、超长的素材名称一路穿透到数据库层才报错。

全局异常处理的工程细节很容易被新手忽略。在没做统一异常处理的项目里,数据库约束冲突、空指针、业务校验失败会返回各种默认错误页,前端拿到的是HTML而不是JSON,导致弹窗里显示一坨看不懂的代码。我的项目里用@RestControllerAdvice配合@ExceptionHandler做了全局异常捕获,给每个业务异常一个明确的错误码和信息。一个非常实用的技巧是:业务异常用自定义BusinessException,在Service层主动抛出并带上用户能看懂的错误信息;系统异常统一返回"系统繁忙"而不泄露堆栈信息,避免技术人员信息暴露和堆栈详情被用户端看到。当然我自己排查问题时,真正的堆栈都打在了服务端日志里。

4. Vue前端:从工程搭建到页面落地

4.1 用Vue CLI搭建工程化开发环境

前端工程的起步是用Vue CLI把架子搭起来。这里有个版本匹配的坑大家一定注意:Vue CLI 5.x生成的默认工程基于Vue 3,而Vue 2的项目需要指定版本约束。这套系统我实际用的是Vue 3 + Element Plus的组合,Element Plus是组件库,它的表格、表单、弹窗、上传组件能省下大量重复的样式的代码。搭建的完整步骤如下:

# 安装Vue CLI工具(已安装可跳过) npm install -g @vue/cli # 创建项目 vue create material-frontend # 选择Vue 3, 手动勾选Router、Vuex、Axios # 进入项目目录 cd material-frontend # 安装Element Plus组件库 npm install element-plus --save # 安装Axios请求库 npm install axios --save

npm install这个环节有个高频低级错误是权限问题。Linux和macOS上全局安装包需要sudo权限,如果报EACCES错误,不要硬闯去改/usr/lib的权限,配置npm全局安装目录到用户目录才是正解。国内网络环境下npm install官方源确实慢,换淘宝镜像谁用谁知道,但要注意镜像源可能滞后最新版本,安装包版本不一致时建议锁定package.json里的版本号。工程搭建好后,用import ElementPlus全局注册,在main.js里use一下组件库和路由实例,基础骨架就跑起来了。

4.2 素材列表页遇到的组件拆分思路

列表页样式的整体排布逻辑是:顶部一个筛选栏,包含类型下拉框、分类选择器、关键词输入框和搜索按钮;主体区域是一整块卡片栅格,每个卡片展示素材缩略图、名称、类型标签和操作按钮。把这块拆成组件来设计,会清晰很多——MaterialFilter负责筛选栏,MaterialCard负责单个素材卡片,MaterialPreview负责点击卡片后的预览抽屉,MaterialUpload负责上传弹窗。

组件之间通信上我用了两种最朴素可靠的方式,一是父组件给子组件传props,二是子组件通过emit事件通知父组件。比如筛选栏里的搜索条件变化,MaterialFilter内部维护表单数据,点击搜索按钮后把查询参数emit到父组件页面,父组件拿着新参数重新请求列表接口。素材卡片上的"删除"按钮被点击后,MaterialCard emit出delete事件,父组件拦截弹出确认框,用户确认后调删除接口,同时刷新列表。这比上来就引入Pinia/Vuex强多了,只有数据在完全不相关的页面间流转时才需要全局状态管理,单独的列表页到素材详情页之间的传递用路由参数就够了。

列表页请求数据的部分,在onMounted生命周期里调用分页接口,拿到数据后更新列表数组和总条数。组件加载期间给表格区域加一个v-loading指令,这是Element Plus的加载遮罩,效果是数据没返回时展示一个半透明loading层,防止用户误以为页面卡死。前端接受接口返回的数据结构是统一的Result对象,接口返回的records数组是当前页的列表数据,total字段用来渲染分页组件的总页数。分页组件页码变化时触发handlePageChange,带上新的pageNum和pageSize再次请求接口。

4.3 Axios封装与路由权限控制

Axios不能裸着用,这算是我的一条铁律。所有请求都要经过一个统一的request实例,在实例里配置baseURL、超时时间、请求拦截器和响应拦截器。请求拦截器的作用是每个请求发出前自动带上token,从localStorage取出JWT塞进请求头的Authorization字段;响应拦截器的作用是统一处理错误码,后端返回401时自动跳转到登录页,业务错误码非同200时弹一个ElMessage提示错误信息。这样一来页面的业务代码只管取数据渲染,所有鉴权和异常提示都在拦截层解决,代码干净太多。

路由设计上,路由表分为公共路由和需要鉴权的业务路由,由于采用Vue Router的beforeEach导航守卫做登录验证。核心逻辑是读取当前路由的meta.requiresAuth字段,如果需要登录但本地没有token,就一律重定向到/login页面。还加了动态标题的细节:每个路由配置里meta.title字段存页面标题,导航守卫里执行document.title赋值,用户看到浏览器的标签页标题也跟着路由切换。素材预览页和素材上传页做成路由懒加载,组件实际路由命中时才加载对应的JS文件,首屏体积明显更小,控制台的请求加载时长肉眼可见地降下来了。

5. 部署上线:前后端分离项目的工业级落地

5.1 后端打包与服务器启动运维

后端部署可以直接贴出来几个关键步骤。Maven打包是用package指令,这一步会在target目录下生成一个jar文件,但这个jar默认不带依赖,直接扔服务器上启动会ClassNotFound报错。必须在pom.xml里配置spring-boot-maven-plugin插件,它打包时会把所有第三方依赖一起折叠进可执行jar里。这个插件默认会在mainClass参数里自动识别主启动类,所以一般配置几行就够。

<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>

服务器上启动jar包,我一般用bash脚本管理启停。最基本的启动命令是nohup java -jar material-server.jar > app.log 2>&1 &,意思是程序以守护进程方式在后台跑,输出日志写入app.log文件。启动后先别高兴,马上用tail -f app.log盯住启动日志,等看到"Started Application in xx seconds"这行才代表启动成功。生产环境我不推荐直接裸java -jar,配一个systemd服务或用Docker都会让运维管理更规范,但简单的内部系统用脚本管理也完全够用。启动前还要检查端口是否被占用,lsof -i:8080能快速查出来。

5.2 前端构建与Nginx反向代理配置

前端构建太简单了,npm run build会在项目里生成一个dist目录,目录里是压缩好的HTML、CSS、JS文件。这个dist目录不能直接双击index.html在浏览器里打开,因为Vue构建出的应用是基于路由的SPA,直接file://协议访问根本加载不出页面,而且接口请求也必然跨域。正确姿势是把dist整个目录扔给Nginx托管。

Nginx配置文件里有几个关键点。第一个是静态资源托管,location /配置root指向dist目录,try_files $uri $uri/ /index.html,这句try_files是SPA路由的核心,确保用户刷新某个子路由时Nginx把请求重写到index.html,由前端路由接管页面展示。第二个是接口代理,前端请求的后端接口地址统一以/api开头,Nginx配置location /api/把请求转发到后端服务地址,这样浏览器请求同源的地址,Nginx转发给后端,规避了跨域问题:

server { listen 80; server_name material.example.com; # 托管前端静态文件 location / { root /var/www/material-frontend/dist; index index.html; try_files $uri $uri/ /index.html; } # 反向代理后端API location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 上传文件大小限制 client_max_body_size 200m; }

这里有个我真实踩过的坑:proxy_pass http://127.0.0.1:8080/和proxy_pass http://127.0.0.1:8080两者行为完全不同。带末尾斜杠的写法会把location前缀/api/从请求路径里剥离后再转发到后端,比如前端请求/api/material/page,后端实际收到的路径变成/material/page;不带斜杠则保留完整路径,后端Controller里如果ClassMapping写的是/api/material,就会匹配不上导致404。很多初学Nginx的朋友在这个字符细节上卡一下午。另一个要命的问题是client_max_body_size默认只有1MB,不上传功能则没事,一旦在页面上传几十MB的视频,Nginx直接返回413错误,这个配置必须手动调大,而且注意我之前在后端SpringBoot里的配置spring.servlet.multipart.max-file-size也要同步调大,两个限制同时生效,只调一个都不行。

5.3 数据库初始化与部署前检查清单

数据库脚本的初始化,我用MySQL命令行或Navicat执行提前准备好的init.sql,里面包含建库语句、建表语句和初始分类数据。执行前务必要确认字符集,建库语句里指定utf8mb4而不是utf8,因为utf8在MySQL里只支持最多3字节的字符,存emoji或生僻字会报错,utf8mb4才是完整的Unicode支持。表结构如果需要更新,本地开发环境改完表结构后一定要生成增量脚本推送到生产环境执行,千万不要直接在生产环境手动改表,改完和研发环境不同步,下次部署Schema就乱了。

整个系统部署上线的完整流程,我用一张检查清单来收尾,每次发布前逐条过一遍:

检查项操作确认说明
数据库连接配置确认SpringBoot的application.yml中数据库地址、账号、密码生产库的配置不要和本地一致
MinIO服务状态curl http://minio-address/minio/health/live确认服务存活通配符URL可先探测连通性
后端打包确认mvn package成功,target下生成完整boot jar打包前先跑测试用例
前端构建npm run build成功,dist目录产物完整构建完成的静态文件先本地nginx跑一遍
反向代理配置nginx -t 通过语法检查修改配置后reload生效
端口与防火墙确认8080后端端口、80前端端口未被防火墙拦截云服务器需在安全组里放行
日志路径确认日志输出路径有写权限,logback配置与启动脚本一致日志挂在的事实比功能上线还重要
定时备份数据库备份脚本每晚会执行至少确保发布前有一次全量备份

部署环节最容易翻车的不是代码而是环境,这个清单能帮你把大部分低级失误挡在门外。我个人的经验是先部署生产环境前,点开素材列表接口返回一条真实数据看JSON格式,确认前端能解析,再点上传按钮传个小图验证链路通不通,最后再传一个大视频,从上传进度到封面生成全流程走一遍。冒烟测试不要跳步骤,很多时候接口通但MinIO桶权限不通,页面图片全是裂图,这种坑只有走完整链路才暴露。

6. 常见问题与排查实录

部署这套系统时,我把真实遇到最多的问题以及排查思路整理成了一张速查表,特别是把那些网上搜了大半天也找不到明确答案的边缘问题写清楚。这里面的场景都是我实际跑过的,掌握这些问题等于提前预习避坑指南。

问题现象原因分析解决思路与操作
前端请求接口报CORS跨域错误后端未开启跨域支持方案一:通过Nginx代理后端解决,前端请求同源;方案二:后端配置CorsFilter
刷新页面后404Vue Router history模式未在Nginx配置try_filesNginx location /里配置try_files $uri $uri/ /index.html
上传大文件时提示413Nginx或SpringBoot的multipart大小限制未调整同时调大Nginx的client_max_body_size和SpringBoot的spring.servlet.multipart.max-file-size
上传视频后封面一直空白@Async异步任务异常未被捕获检查异步方法日志,确认FFmpeg命令执行的服务器环境是否安装了FFmpeg依赖
列表查询接口响应慢分页查询未命中索引或跨表查询过多用EXPLAIN查看执行计划,确认idx_type_category索引是否命中,超200万条记录后考虑分表
MinIO链接访问报SignatureDoesNotMatch服务器系统时间与MinIO时间偏差过大同步服务器时间,安装ntpdate并设置定时同步
MyBatis分页总数不对PageHelper插件位置使用错误检查startPage调用后紧跟的第一个查询是不是目标Mapper方法
数据库连接超时无法连接MySQL未启动,或socket文件路径不一致检查mysql服务状态、连接账号权限,确认jdbcUrl与mysql.sock路径对应
素材图片加载缓慢图片未做压缩或缩略图直接引原图上传流程中做强校验,列表页展示缩略图URL,详情页才展示原图地址
修改表结构后服务启动失败数据库表和实体类字段不对应对比实体类字段和表结构,重点检查时间字段类型和nullable约束

日志排查的第一步永远是先看日志,先确认错误发生在请求入口还是后端处理,还是文件存储环节。我一般会用tail -f命令同时打开后端日志和Nginx错误日志,一个请求进来,观察到Nginx把请求转给后端,再看到后端有对应的日志输出,链路就基本通了。很多问题定位不到本质上是因为日志级别设置太高,DEBUG级别下能看到的参数内容都藏在日志里,排查问题时把日志级别临时调成DEBUG,确认问题后再调回INFO。

最后分享几个我再做一次会坚持的做法

多媒体素材管理这个项目做完,我得说这套前后端分离架构经受住了真实使用的考验。最有价值的经验之一是把文件存储和数据库彻底分离,MinIO独立部署后,系统迁移服务器时直接把整个存储目录搬过去,数据库里重新配一下地址就完事,省了一大堆文件复制工作。

另一条经验是在前端请求串接时坚持用统一的路径前缀约定。前端所有文件相关请求都走/api/前缀,上传的回显地址单独存在一个文件中继路径下,这样后端将来换对象存储时,只需要改一个反向代理的转发目标就行,页面代码完全不需要动。这种面向扩展的设计在实际维护中特别值钱。

最后关于这套项目后续的扩展方向,素材检索是第一个值得加强的模块。当前版本只有名称模糊搜索,后续可以引入标签体系和全文检索,让用户能按语义搜索素材,那套设计思路上来就是另一个层次的复杂度。如果需要了解素材管理、MinIO存储或前后端部署中某一块更深入的细节,欢迎在评论区说说你的具体场景,我可以挑几个高频的问题单独展开写。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询