最近半年陆陆续续帮不少学弟学妹看过SpringBoot+Vue3的毕设项目,发现文档管理这一类的题目出现频率特别高。原因也好理解:业务边界清晰、功能点好展示、技术栈主流,做出来既有完整的前后端交互,又有文件读写这种实打实的业务逻辑,答辩证的时候特别有话讲。
这次分享的是一个基于Java SpringBoot + Vue3 + MyBatis + MySQL的江理工文档管理系统源码,前后端分离架构。简单来说,它解决的是高校里课程资料、实验报告、社团文件“散落在各个QQ群、U盘、网盘里找不到”的痛点,做成一个统一的在线文档管理平台。系统包含用户登录认证、文件上传下载、分类管理、在线预览、后台管理等完整模块,适合拿来当毕业设计、课程设计,也适合想完整跑通一个前后端分离项目的同学练手。
我把整个项目的开发过程、数据库设计、核心代码实现、以及部署踩坑记录全部梳理一遍,尽量把设计时为什么这么做的理由也讲明白,而不是光贴一堆代码。
1. 项目定位与技术选型思路
1.1 文档管理系统的核心业务拆解
做任何项目之前,先把业务边界摸清楚。文档管理系统听起来很宽泛,但落到高校场景里,核心就是四个字:存、找、管、下。
“存”是文件上传,要解决文件存哪里、怎么命名、怎么避免重名;“找”是文件检索,最基本的按文件名模糊搜索、按分类筛选;“管”是文件维护,分类结构怎么组织、谁能删除和修改;“下”是文件下载,同时记录下载次数这类统计信息。如果把范围再扩大一点,还可以加用户管理、操作日志、回收站等模块,但核心骨架就是上面四条。
我给这个系统定的功能清单是这样的:
- 用户模块:注册、登录、JWT令牌鉴权,区分普通用户和管理员
- 文件模块:上传、下载、删除、重命名、文件列表分页查询
- 分类模块:多级分类维护,文件挂载到分类下
- 预览模块:图片和PDF浏览器内直接预览,Office文件方案见后面说明
- 管理模块:管理员可管理用户状态、查看全站文件、统计下载量
这个功能规模对毕设来说不多不少。少了显得单薄,答辩时没什么可演示的;太多了开发周期失控,光是调试Bug就能让人崩溃。我见过有些同学一上来就想做在线编辑文档、多人协同,这种需求对一个React+WebSocket+CRDT团队来说都够呛,何况一个人搞前后端。先做好核心,把骨架搭稳,比什么都重要。
1.2 为什么选SpringBoot+Vue3+MyBatis这套组合
这套技术栈放在2024年依然是国内Java后端、以及高校项目里最主流的选择,几乎没有之一。
后端选SpringBoot,理由不必多说:自动配置让项目从零到能跑只需要几分钟,内嵌Tomcat省去部署Web容器的麻烦,生态里随便一搜就是海量解决方案。最关键的是,SpringBoot对各种场景的starter封装得非常完善,比如文件上传、参数校验、拦截器,这些在文档系统里全都要用上。
前端选Vue3是顺势而为。Vue2还在用Options API那一套,Vue3的Composition API在处理复杂业务逻辑时明显更顺手,再加上<script setup>语法糖,写起来简洁直接。UI组件库选Element Plus,表格、表单、上传、分页这些组件基本都是现成的,不用从零手搓样式,开发效率直线上升。
持久层选MyBatis而不是JPA,理由很实际:文档管理系统的查询条件组合比较灵活——按文件名模糊查、按分类查、按上传者查、按时间范围查,这些动态查询用MyBatis的动态SQL来写非常顺手。而且MyBatis的SQL是手写的,可控性强,出现问题排查起来直观。配合分页插件PageHelper,分页查询只需要一行代码。
数据库选MySQL没什么悬念,开源免费、资料多、高校机房和毕业设计场景的标配。8.0版本在性能和功能上比5.7完善不少,我这次用的是8.0。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与后端核心实现
2.1 数据库表结构设计,一张图讲清楚
表设计是整个系统的地基,地基打不好,后面写代码处处别扭。我设计了四张核心表:用户表、分类表、文件信息表、操作日志表。
用户表存账号密码和角色:
sql复制CREATE TABLE `sys_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '登录账号',
`password` varchar(100) NOT NULL COMMENT '密码(BCrypt加密)',
`nickname` varchar(50) DEFAULT NULL COMMENT '显示昵称',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像地址',
`role` tinyint NOT NULL DEFAULT '1' COMMENT '角色:0管理员 1普通用户',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1正常 0禁用',
`create_time` datetime NOT NULL COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
密码字段我用BCrypt加密存储,Spring Security里的BCryptPasswordEncoder就能生成和校验。有些同学直接把明文密码放数据库里,答辩时老师一问数据安全就卡壳,不太好。
分类表设计成支持多级分类的结构:
sql复制CREATE TABLE `doc_category` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL COMMENT '分类名称',
`parent_id` bigint NOT NULL DEFAULT '0' COMMENT '父分类ID,0表示顶级',
`sort_order` int DEFAULT '0' COMMENT '排序权重',
`create_time` datetime NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文档分类表';
文件信息表是核心表,字段设计上有个关键决策:数据库里存的是文件元信息,而不是文件本身。文件实体放在服务器磁盘上,数据库里只记录路径、大小、类型这些描述性字段。这里有个容易踩坑的点:很多初学者喜欢把文件转成二进制BLOB存数据库,短小文件无所谓,一旦有几十MB的视频文件,数据库瞬间膨胀,查询性能直线下降。
sql复制CREATE TABLE `doc_file` (
`id` bigint NOT NULL AUTO_INCREMENT,
`file_name` varchar(255) NOT NULL COMMENT '原始文件名',
`file_path` varchar(500) NOT NULL COMMENT '存储路径',
`file_size` bigint NOT NULL COMMENT '文件大小(字节)',
`file_type` varchar(50) DEFAULT NULL COMMENT '文件类型(扩展名)',
`category_id` bigint DEFAULT NULL COMMENT '所属分类ID',
`uploader_id` bigint NOT NULL COMMENT '上传者ID',
`download_count` int NOT NULL DEFAULT '0' COMMENT '下载次数',
`create_time` datetime NOT NULL,
`update_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_category` (`category_id`),
KEY `idx_uploader` (`uploader_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件信息表';
索引设计这块,我在分类ID和上传者ID上各加了一个普通索引。原因很简单:文档系统最常见的查询是“点开某个分类看文件列表”,不带索引的话随着数据量增加,全表扫描会越来越慢。下载计数用download_count字段而不是单独一张下载记录表,是为了查询统计时少一次连表,代价是拿不到每次下载的明细,对毕设系统来说够用了。
2.2 SpringBoot项目结构与MyBatis关键配置
后端项目我按标准的分层结构组织,不过没有死板地用五层架构,而是精简到了controller、service、mapper、entity、common这几个包。对于这种规模的项目,五层架构反而是负担,写起来全是无意义的接口转发。scaffold(脚手架)类的项目建议去参考若依(RuoYi)的分层风格,但不要照搬,一定要根据自己的业务量做减法。
application.yml里的关键配置是这样的:
yaml复制server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/doc_manage?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
username: root
password: 123456
servlet:
multipart:
max-file-size: 100MB
max-request-size: 100MB
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.docmanage.entity
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
这里有两个细节值得说一下。
第一,serverTimezone=Asia/Shanghai一定要显式指定。MySQL 8.0的驱动对时区校验很严格,不设置的话大概率报server time zone value is unrecognized的错。第二,map-underscore-to-camel-case开启后,数据库的create_time字段就能自动映射到Java实体里的createTime属性,不用手动写一堆resultMap映射,写代码时清爽很多。
MyBatis的XML文件放在resources/mapper目录下,文件名跟Mapper接口一一对应。举个例子,文件查询的Mapper接口方法定义是:
java复制List<DocFile> selectFileList(@Param("fileName") String fileName,
@Param("categoryId") Long categoryId,
@Param("uploaderId") Long uploaderId);
对应的XML:
xml复制<select id="selectFileList" resultType="com.docmanage.entity.DocFile">
select * from doc_file
<where>
<if test="fileName != null and fileName != ''">
and file_name like concat('%', #{fileName}, '%')
</if>
<if test="categoryId != null">
and category_id = #{categoryId}
</if>
<if test="uploaderId != null">
and uploader_id = #{uploaderId}
</if>
</where>
order by create_time desc
</select>
动态SQL的<where>标签会自动处理第一个条件的and前缀,这个细节能省去很多“多了一个and报SQL语法错误”的调试时间。
2.3 分页插件PageHelper的用法与一个隐藏内存问题
这个项目里列表查询全部走MyBatis分页,我用的PageHelper。用法极简,查询之前调一行代码:
java复制PageHelper.startPage(pageNum, pageSize);
List<DocFile> list = docFileMapper.selectFileList(...);
PageInfo<DocFile> pageInfo = new PageInfo<>(list);
PageHelper.startPage只对紧接着的下一条SQL查询生效,这是它的设计哲学——用ThreadLocal存储分页参数,SQL执行完自动清除。好处是用起来无侵入,代价是如果你在startPage和查询之间多调用了一次别的SQL,分页参数会被那次查询消费掉,导致目标查询没分页。所以这两个操作必须紧挨着写,中间不要插任何数据库操作。
PageInfo封装了总记录数、总页数、当前页码这些分页元数据,前端分页组件需要的信息都在里面,直接把pageInfo塞进返回结果就行。
顺手提一个PageHelper的版本坑:SpringBoot 3.x对应的是pagehelper-spring-boot-starter 2.x版本,SpringBoot 2.x用1.4.x版本。starter版本跟SpringBoot主版本不匹配的话,分页SQL可能不生效,查出来全表数据但页信息全是错的。遇到这种问题先检查starter版本,不要一上来就怀疑自己代码写错了。
2.4 文件上传下载实现:从MultipartFile到下载响应
文件上传是文档系统的门面功能,做得不好直接影响使用体验。我核心实现思路是这样的:
控制器接收前端传来的MultipartFile,先做类型和大小校验,然后生成存储路径,把文件写入服务器磁盘,再把文件元信息记录到数据库。
java复制@PostMapping("/upload")
public Result upload(@RequestParam("file") MultipartFile file,
@RequestParam(value = "categoryId", required = false) Long categoryId) {
if (file.isEmpty()) {
return Result.error("上传文件不能为空");
}
// 校验扩展名白名单
String originalName = file.getOriginalFilename();
String extension = StringUtils.getFilenameExtension(originalName);
if (!allowedExtensions.contains(extension.toLowerCase())) {
return Result.error("不支持的文件类型");
}
// 使用UUID+原始文件名拼接存储名
String storeName = UUID.randomUUID().toString().replace("-", "") + "." + extension;
String datePath = new SimpleDateFormat("yyyy/MM/dd").format(new Date());
File dest = new File(uploadDir + "/" + datePath, storeName);
if (!dest.getParentFile().exists()) {
dest.getParentFile().mkdirs();
}
file.transferTo(dest);
// 记录元信息
DocFile docFile = new DocFile();
docFile.setFileName(originalName);
docFile.setFilePath(datePath + "/" + storeName);
docFile.setFileSize(file.getSize());
docFile.setFileType(extension);
docFile.setCategoryId(categoryId);
docFile.setUploaderId(currentUserId());
docFileService.saveFile(docFile);
return Result.success(docFile);
}
这里有几个细节是踩坑之后才补上的。原始文件名不能直接作为存储文件名,一是可能重名覆盖,二是中文文件名在跨平台传输时容易出现乱码问题。所以我用UUID作为存储文件名,原始名称只存在数据库里,返回给用户。文件路径末端在搞遍历攻击,就不会因为文件名被恶意构造而踩到路径穿越漏洞了——虽然毕设不太会被真攻击,但养成习惯总是好的。存路径按年/月/日分目录,避免单个目录下文件数量爆炸,查找和备份都方便。
文件下载接口就简单一点,先根据ID查元信息,再通过路径把文件读出来写进响应流:
java复制@GetMapping("/download/{id}")
public void download(@PathVariable Long id, HttpServletResponse response) throws IOException {
DocFile docFile = docFileService.getById(id);
if (docFile == null) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
// 下载计数 +1
docFileService.increaseDownloadCount(id);
File file = new File(uploadDir + "/" + docFile.getFilePath());
if (!file.exists()) {
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return;
}
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition",
"attachment;filename=" + URLEncoder.encode(docFile.getFileName(), "UTF-8"));
Files.copy(file.toPath(), response.getOutputStream());
}
下载响应里Content-Disposition头部的文件名做了URL编码,这是解决中文文件名下载乱码的关键。不编码的话,浏览器拿到响应头里带中文的文件名会显示成一串乱码,甚至直接下载失败。
在线预览这块,图片和PDF是最简单的,浏览器原生支持,用<img>标签和<iframe>直接渲染/preview/{id}接口返回的文件流就行。Office文件(Word、Excel)比较麻烦,浏览器不能直接打开。我用了LibreOffice做服务端转换,把doc、docx、xls、xlsx转成PDF再输出给前端,效果不错但部署时需要在服务器上装LibreOffice环境。如果觉得太复杂,也可以用KKFileView这类开源在线预览项目来对接,它就是专门干这个的。
3. Vue3前端开发实现
3.1 用Vite初始化Vue3项目并集成Element Plus
前端我用的Vite作为构建工具,创建命令很简单:
bash复制npm create vite@latest doc-manage-web -- --template vue
Vite创建的项目结构非常干净,然后把依赖装上:
bash复制npm install element-plus
npm install vue-router@4
npm install pinia
npm install axios
Element Plus在Vue3项目里的引入方式有两种:全量引入和按需引入。毕设项目规模不大,直接全量引入省心:
javascript复制import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus)
app.mount('#app')
按需引入要用unplugin-auto-import和unplugin-vue-components两个插件,打包体积确实小一些,但对毕设项目来说收益不大,反而增加配置复杂度。我建议全量引入,把精力留在业务代码上。
前端项目目录结构我按功能划分:
text复制src/
api/ # 接口请求封装
assets/ # 静态资源
components/ # 通用组件
router/ # 路由配置
stores/ # Pinia状态管理
utils/ # 工具函数(axios实例、token处理等)
views/ # 页面组件
3.2 Axios二次封装与接口统一处理
Axios如果不做封装,每个页面里重复写headers: { token: ... }和错误弹窗,那代码会非常难看。我封装了一个统一的请求实例:
javascript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request = axios.create({
baseURL: '/api',
timeout: 30000
})
// 请求拦截器:自动携带token
request.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers['Authorization'] = 'Bearer ' + token
}
return config
})
// 响应拦截器:统一处理错误码
request.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
if (res.code === 401) {
localStorage.removeItem('token')
router.push('/login')
}
return Promise.reject(new Error(res.message))
}
return res
},
error => {
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default request
这套封装的好处是:每个请求方法只需要关心自己的业务参数和返回数据,token携带、错误提示、登录失效跳转这些横切逻辑全部收敛在一个文件里,做完这些冗余工作后,各页面写API调用就清爽了:
javascript复制// api/file.js
import request from '@/utils/request'
export function getFileList(params) {
return request.get('/file/list', { params })
}
export function uploadFile(data) {
return request.post('/file/upload', data, {
headers: { 'Content-Type': 'multipart/form-data' }
})
}
拦截器里的401处理值得特意提一下:后端返回的登录失效状态码,在拦截器里统一清token、跳登录页,这样即使有十个页面调用了受保护的接口,也只需要写这一处逻辑。
3.3 路由守卫与文件管理页面实现
Vue Router的导航守卫用来控制页面访问权限:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
const isLoginPage = to.path === '/login'
if (!token && !isLoginPage) {
next('/login')
return
}
if (token && isLoginPage) {
next('/')
return
}
next()
})
逻辑很简单但不遗漏任何一种情况。没登录访问任意页面都会弹回登录页,已登录却手动输入/login地址的会被导向首页,避免出现“登录了还能看到登录页”的别扭状态。
文件列表页是整个前端工作量最大的页面。布局上左侧是分类树,右侧是文件表格。表格列设置成:文件名、大小、类型、上传者、上传时间、下载次数、操作。操作列放下载、重命名、删除三个按钮,管理员额外多一个用户管理入口。
由于表格里的文件名只展示原始名称,我加了一个列:文件名后面跟一个小图标,点击图标调用预览接口,在新标签页打开预览。Element Plus的el-table组件支持default-sort和自定义列模板,文件大小的格式化直接写个全局过滤器就行。
上传功能用Element Plus的el-upload组件,设置action为后端上传地址,name字段匹配后端的MultipartFile参数名:
html复制<el-upload
action="/api/file/upload"
:headers="uploadHeaders"
:data="uploadData"
name="file"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
drag
multiple>
<div>拖拽文件到此处,或点击选择文件</div>
</el-upload>
前端校验除了后端的三重验证之外,还能加一道拦截,如果用户传的文件类型压根不在允许范围内,前端直接弹窗提醒,省得传上去被后端拒了再弹一次错。
3.4 Vite代理解决跨域问题
前后端分离项目开发时必然遇到跨域:前端跑在5173端口,后端在8080端口,浏览器会拦截跨域请求。解法有两个:后端加CORS配置,或者前端开发服务器做代理转发。
我推荐用Vite代理,开发环境和生产环境之间无缝切换:
javascript复制// vite.config.js
export default {
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
}
这样前端请求/api/file/list会被Vite转发到http://localhost:8080/api/file/list,浏览器看到的始终是同源请求,跨域问题直接消失。而且这个配置只影响开发环境,生产环境部署时用Nginx做类似的反向代理就行,数据中心侧一台Nginx就能同时搞定静态文件托管和API转发,前后端挂到同一个域名下。
4. 部署联调实录与常见问题速查
4.1 本地环境从零搭建MySQL数据库
这里说一句关于MySQL安装的实在话:别看网上教程一大把,真正稳定装好MySQL 8.0并且能远程连接上的,比例不算高。如果你发现装好之后本地访问正常、后端却连不上,八成是下面这几个环节出了问题。
MySQL 8.0安装时要注意字符集选utf8mb4而不是utf8,虽然utf8mb4是MySQL 8.0的默认字符集,但我还是建议建库时显式指定,防止某些可视化工具跳过初始化设置:
sql复制CREATE DATABASE doc_manage DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
JDBC连接串里已经配了characterEncoding=utf8,对应数据库端utf8mb4,两边一配合,中文存取就不会出现乱码。
还有一个很容易被忽略的步骤:MySQL 8.0默认的认证插件是caching_sha2_password,如果你的JDBC驱动版本比较老,连接时会报Public Key Retrieval is not allowed。解法是换用8.0.x版本的mysql-connector-java,或者在连接串后面加allowPublicKeyRetrieval=true。我建议直接用新版驱动,加参数只是治标。
如果后端需要连远程的MySQL,还得检查用户表的host字段。MySQL默认创建的root用户host是localhost,远程连不进来是正常的。需要手动授权:
sql复制CREATE USER 'docuser'@'%' IDENTIFIED BY '123456';
GRANT ALL PRIVILEGES ON doc_manage.* TO 'docuser'@'%';
FLUSH PRIVILEGES;
这里的%表示允许任意IP连接,局域网部署场景会用到,生产环境就不用这样配了。
4.2 后端联调时最常见的三个报错
把整个联调过程中出现的报错按频次排个序,前三名基本固定,而且都有比较通用的排查套路。
第一个是跨域报错,浏览器控制台会显示CORS policy相关的错误信息。如果你用了Vite代理还是报跨域,先确认代理是否真的生效——请求路径是否带上了/api前缀,target是否写对。另外SpringBoot后端如果也加了@CrossOrigin注解,有时候会和代理配置产生重复授权的问题,两个方案二选一就行,不要同时用。
第二个是文件上传超限。SpringBoot默认的单文件大小上限是1MB,我们用100MB覆盖了默认值。但是要注意,Nginx在上游如果配了client_max_body_size默认是1m,文件稍微大一点就会报413错误。所以如果后端部署在Nginx后面,这个参数要同步调大:
nginx复制client_max_body_size 100m;
第三个是数据库批量插入或者时间字段相关的报错,这类问题定位最快的方法是看日志。我在application.yml里开了StdOutImpl日志实现,MyBatis执行的所有SQL和参数值都会打印到控制台。遇到SQL报错,把打印出来的SQL直接复制到数据库客户端里执行一遍,很多时候问题瞬间就暴露了。排查完记得把log-impl注释掉,或者改回Slf4jImpl,不然控制台日志会被刷得飞起。
4.3 安全与细节处理:Token校验、文件类型白名单、SQL注入
毕设项目的安全意识是加分项,答辩时提一句能让老师觉得你考虑问题全面。
登录接口生成JWT令牌时一般设置过期时间,我用的jjwt库,密钥随便写一个复杂的字符串就行。关键在于后端的拦截器需要排除登录和注册接口,其他所有接口都验证token。用SpringBoot的HandlerInterceptor实现:
java复制public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
String token = request.getHeader("Authorization");
// 校验token,失败则返回401
// 成功则把用户ID放入request attribute
return true;
}
}
注册到拦截器时注意放行白名单的写法:
java复制registry.addInterceptor(authInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/auth/login", "/auth/register", "/file/preview/**");
文件上传的类型校验用白名单而不是黑名单。黑名单的思路是“我知道哪些文件危险,我去拦它们”,但文件扩展名千奇百怪,很难穷举完;白名单反过来,只允许图片、PDF、Word、Excel这些明确需要的类型,其他一律拒绝,简单又可靠。
SQL注入防护这块,重点记住MyBatis的#{}和${}的区别。#{}使用预编译占位符,参数值根本不会拼接到SQL语句里,所以安全;${}是字符串拼接,存在注入风险。不要用${}拼接查询参数,这是底线。order by子句如果想动态传列名用${},也必须在后端做白名单校验,比如只允许传入“create_time”“file_size”这种写死的列表。——排序那到底要不要允许任意列,我的建议是干脆不开放,后端固定写几个排序字段,前端传枚举值进来映射到固定能用的那几列,从源头就堵住。
4.4 这个系统后续还能怎么扩展
如果时间充裕想在这套系统上再加分,我会推荐三个方向。一是文件回收站功能,删除的文件先进入回收站,30天后自动清理或者支持手动恢复,这是可以体现数据库事务和定时任务能力的模块。二是操作日志模块加上简单的统计分析,用ECharts展示每日上传量和文件下载排行,可视化在答辩现场的效果很好。三是对接云存储,把文件从本机磁盘换到阿里云OSS或者腾讯云COS,代码层面只需要改造FileStorageService这一个接口的实现,这个“面向接口设计”的改造点本身就可以拿来当答辩亮点。
最后说几句实在话
做完这个项目最大的感受是:前后端分离项目的难度不在于某个技术点有多深,而在于整个链路串起来的细节太多了。从数据库表设计到后端的文件流处理,从前端的Axios封装到跨域代理,再到最终的Nginx部署,任何一个环节断掉,整个系统就跑不起来。但恰恰是这些琐碎的细节堆在一起,把一个学生从“会写接口”推到了“能做完整项目”的程度。
给正在做类似项目的同学一个建议:代码写之前先用两天把数据库表和接口文档定下来,表结构一旦建好、接口路径一旦定好,前后端就能并行开发。前端同学拿着接口文档写页面,后端同学按文档实现接口,联调时冲突至少少一半。对于个人项目,这个习惯也值得坚持,因为你自己写的页面迟早要对接你自己写的接口。
这套系统的完整源码、SQL脚本和部署文档我整理放在一起了,需要的可以直接做参考。有问题也欢迎在评论区交流,看到都会回。
