去年给一家文博机构搭线上历史馆藏系统,我几乎没有犹豫就把技术栈定成了SpringBoot+Vue+MyBatis+MySQL的前后端分离组合。项目交付之后回头看,这个决定省了我大量不必要的麻烦——馆藏类系统本质上就是一个数据密集型后台管理系统,它需要稳定的接口、灵活的检索、可控的权限,而这一套恰好就是SpringBoot生态最擅长的范围。这篇文章不晒截图,把需求拆分、库表设计、后端核心逻辑、前端页面、服务器部署还有几个印象深刻的坑完整走一遍。如果你正准备做毕业设计,或者第一次上手前后端分离项目,这个案例可以直接跟着做;如果你想在自己服务器上快速跑一个能用的历史馆藏系统,部署教程部分可以单独抄作业。
1. 项目从Excel到在线化:馆藏系统到底要解决什么问题
1.1 真实使用场景中的三个痛点
接手这个项目之前,对方的馆藏管理工作基本靠Excel加共享文件夹。表面看流程能跑,实际上问题已经攒了一堆。
第一个痛点是数据一致性问题。Excel文件放在共享目录里,几个录入员同时打开,谁后保存谁覆盖。遇到过录入员A花一下午整理了一批新入藏文物编号,结果录入员B随手关掉弹窗,整批数据没了。这种事故发生后复盘都找不到责任人,因为文件最后保存时间可能都变了。
第二个痛点是资产状态不透明。文物不是一直在库房里放着,会有借展、修复、盘点这些流转动作。在Excel体系里,状态靠一个"备注"列手写,借出去的文物忘记改备注是家常便饭。领导问"那批唐代瓷器现在在哪个馆巡展",往往要打电话发消息问一圈才能确认。
第三个痛点是检索效率太低。按名称、朝代、材质、编号筛选,在几千行Excel里还能勉强用筛选功能,到了上万条记录就非常吃力。更别说封面照片和条目之间只能靠文件名对应,图片一多就乱。
这三个痛点其实代表了所有中小型资产管理系统的通病。所以做线上化改造时,核心目标不是"把Excel搬到网页上",而是通过独立的数据库表、状态字段、检索接口,让数据的准确性、实时性、可追溯性一次到位。
1.2 功能模块拆分:不做大而全,只做核心闭环
需求梳理会上对方提了很多想法,比如3D文物展示、VR展厅、在线文物修复流程审批,听起来都很炫。但我最后把第一期范围收在了六个模块,理由很直接:先把资产账本管清楚,展示类功能后续随时可以往上加,而数据基础不牢,什么上层功能都是空中楼阁。
一期落地的模块是这样划分的:
- 登录与权限:管理员、录入员、访客三种角色。访客只能浏览和检索,录入员可以新增编辑文物资料,管理员额外拥有用户管理和借展审批权限。
- 藏品台账管理:文物基础信息的新增、修改、删除、图片上传、详情查看。这是整个系统的核心。
- 分类与检索:按文物类别、朝代、材质、状态做多条件组合查询,支持分页。
- 借展出入库管理:登记借出、归还记录,借出后藏品状态自动变为"外借",归还后恢复"在库"。
- 统计面板:按朝代、类别、状态统计藏品数量,用柱状图和饼图呈现。
- 操作日志:记录谁在什么时间改了哪条数据,便于追溯。
这个范围对一期来说足够形成业务闭环。权限控制保证数据安全,台账管理解决数据一致性问题,检索解决效率问题,借展记录解决资产透明问题,日志解决追溯问题。后面每一个用户能在系统里完成的工作,都对应一个明确痛点。
2. 技术选型复盘:SpringBoot+Vue+MyBatis+MySQL为什么是最省心的组合
2.1 后端:SpringBoot+MyBatis的匹配逻辑
我把后端定为SpringBoot,理由不是因为它"最新最火",而是它在中小型项目中能极大压缩配置成本。SpringBoot自带内嵌Tomcat,打包后一个jar直接跑,不用单独装外部容器;starter依赖体系把常用组件的兼容性提前处理好,pom文件里加依赖就能用。
持久层选MyBatis而不是JPA,当时是认真对比过的。JPA确实写CRUD很快,实体类一标注解就能自动生成SQL,但麻烦也在这——一旦涉及到多条件动态检索、复杂的统计SQL,自动生成的SQL要么没法用,要么得写JPQL还得调方言。馆藏系统里"按朝代+类别+材质组合筛选"这种需求太常见了,SQL的条件数量是动态的,这正是MyBatis动态SQL的强项。XML里写<where>加<if>标签,条件有就拼进去,没有就自动跳过,逻辑清楚,效果直观。
更重要的一点是SQL可控性。MyBatis里每条SQL都是自己写的,性能情况心里有数。JPA在数据量上来之后,经常出现莫名其妙的多表关联查询,排查起来很痛苦。而MyBatis的SQL可以直接在Navicat里跑通再贴进XML,每一步都可控。
2.2 前端:Vue+ElementUI的工程化优势
前端选Vue是顺理成章的。后台管理系统的页面结构高度相似:左侧菜单、顶部栏、内容区表格表单。Vue的组件化开发模式非常适合这类页面,一个藏品列表页可以拆成搜索组件、表格组件、分页组件、弹窗表单组件,各自维护状态,互不干扰。
我用的UI库是ElementUI,它对后台场景的覆盖非常完整。表格有自带的分页、排序、列宽拖动,表单有校验规则,上传组件直接支持图片预览,省掉大量手写样式和交互逻辑的时间。页面大概的布局用栅格系统排一下,标签页、面包屑、对话框都是现成的。
组件选型的另一个考虑是文档和社区。ElementUI的中文文档很全,遇到问题百度或者查Issue基本都能解决。对做项目赶工期的人来说,这不是小事——一个组件要花半天研究用法,项目节奏就全乱了。
2.3 数据库:MySQL 8.0在中小型馆藏项目里的定位
数据库没有悬念选了MySQL 8.0。从部署难度、稳定性、团队熟悉程度几个维度看,MySQL在中小型系统里依然是最稳的选择。馆藏项目的数据库性能要求并不苛刻,一般地市级文博机构几千件到几万件文物,这个量级里MySQL完全不是瓶颈。
真正让我决定用8.0版本而不是停留在5.7的,是几个实在的优势:utf8mb4默认字符集支持所有文字符号,遇到生僻字、特殊符号不会乱码;JSON字段类型可以做灵活的扩展属性;窗口函数对统计报表查询非常方便。比如统计每个朝代的藏品数量,用ROW_NUMBER()窗口函数可以很轻松地做排名,这在旧版本里要写子查询绕来绕去。
有一点要提醒:网上很多教程还在用MySQL 5.7的配置习惯,如果你直接用8.0,注意认证插件是caching_sha2_password,老版本的客户端驱动可能连不上。服务器上安装时选择默认认证方式,后端驱动用最新版JDBC,就不会踩这个坑。
3. 数据库设计与核心表结构:文物台账的数据骨架
3.1 六张核心表的字段规划
数据库设计是这类型项目里最值得花时间的部分。表结构定好了,后面接口和页面都是顺着字段长出来的。当时设计了六张表,核心关系不算复杂,但每个字段都对着业务场景抠过。
先看用户表t_user:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键,自增 |
| username | varchar(50) | 登录名,唯一索引 |
| password | varchar(100) | 密码,加密存储 |
| nickname | varchar(50) | 显示名称 |
| role | varchar(20) | 角色:admin/editor/viewer |
| status | tinyint | 1启用 0禁用 |
| create_time | datetime | 创建时间 |
密码存储没有用MD5,MD5加盐容易被彩虹表撞库,我用的是Spring Security里的BCryptPasswordEncoder,每次生成的哈希都不一样,安全性高不少。
其次是藏品分类表t_category,字段很简单:id、parent_id、category_name、sort。parent_id是为了支持两级分类,比如"瓷器"下面可以有"青花瓷""粉彩瓷"。查询的时候用递归或者程序组装成树形结构。
核心的藏品表t_collection字段最多,这里只列关键部分:
| 字段 | 类型 | 说明 |
|---|---|---|
| collection_no | varchar(50) | 文物编号,唯一索引 |
| name | varchar(100) | 藏品名称 |
| category_id | bigint | 分类ID,逻辑外键 |
| dynasty | varchar(50) | 朝代,如唐代、宋代 |
| material | varchar(50) | 材质 |
| size_desc | varchar(200) | 尺寸描述 |
| weight | decimal(10,2) | 重量(克) |
| source | varchar(200) | 来源 |
| status | tinyint | 1在库 0外借 2修复中 |
| cover_image | varchar(200) | 封面图片路径 |
| description | text | 详细介绍 |
| operator_id | bigint | 最后操作人ID |
| create_time | datetime | 创建时间 |
| update_time | datetime | 更新时间 |
collection_no加唯一索引是我特别坚持的。文物编号相当于身份证号,录入时有重号系统直接报错,比事后去重效率高得多。cover_image字段存的是相对路径而不是Base64图片内容,原因后面部署章节会说。
借展记录表t_lend_record用来追踪文物去向:collection_id、lend_org(借展单位)、lender(借展联系人)、borrow_date(借出日期)、return_date(预计归还)、back_date(实际归还)、status、remark。注意return_date和back_date是两个字段,一个代表计划一个代表实际,不能混用,不然考核的时候说不清楚。
最后一张操作日志表t_operation_log,记录了用户ID、操作动作(新增/修改/删除/借出/归还)、目标资源ID、详情描述、操作时间。用AOP统一记录,这个表在排查数据问题时能起大作用。
3.2 索引、外键与字符集:设计阶段的三个决策
建表时我在索引上做了三件事。第一是collection_no唯一索引,前面说过防重号。第二是(category_id, status)组合索引,因为列表页最常见的筛选场景就是"某个分类下有哪些在库藏品",这个组合索引能直接命中。第三是create_time普通索引,统计面板按时间维度分析数据时会用到。
关于外键,我刻意没有建物理外键,只保留逻辑外键。这个决策可能会被学校里的数据库课程扣分,但实际维护过项目的人都懂:物理外键在删除数据、批量导入、分库分表时会带来一堆麻烦。比如要批量导入一批历史数据,如果分类表还没导完,带外键约束的藏品表根本插不进去。逻辑外键靠程序保证一致性,配合事务可以满足这个项目的需求。
字符集统一用utf8mb4_general_ci。utf8mb4不是默认的utf8,它扩展了4字节字符的支持,生僻字、emoji都能存。我特别用一件包含特殊符号的藏品测过,乱码问题直接没了。排序规则选general_ci是为了处理中文时不区分大小写,比如按名称排序时"张大千"和"张大千"的同名不同字情况不会出乱子。
时间类型全部用datetime,统一记录到秒。程序传值时用Java 8的LocalDateTime,在MyBatis里加一个类型处理器映射,存进MySQL就是标准的yyyy-MM-dd HH:mm:ss。之前见过有人用timestamp,结果2038年问题且时区转换容易出错,这里不推荐。
4. 后端接口与业务逻辑:从登录鉴权到藏品流转
4.1 统一返回体、跨域与JWT拦截器
后端接口设计第一步是定统一返回格式。所有Controller返回的数据都包一层Result<T>,结构是{code: 200, message: "success", data: ...}。这么做的好处很实在:前端axios响应拦截器里只需要判断一次code,200走正常逻辑,401跳登录页,500弹出错误提示,不用每个接口单独处理异常结构。所有异常统一用@RestControllerAdvice捕获,业务异常抛BusinessException,全局处理器转成对应错误码。
登录鉴权用的是JWT方案。用户登录成功后,后端签发一个token返回给前端,token里面带用户ID和角色。之后前端每次请求都在Authorization头里带上这个token,后端写一个拦截器统一校验。
拦截器里有一个细节值得说:JWT解析出来用户信息后,我没有直接塞进HttpServletRequest里拿,而是用ThreadLocal来传递。因为写业务代码时需要频繁获取当前登录用户,写request.getAttribute("loginUser")太啰嗦。定义一个UserContext工具类,拦截器里解析完就UserContext.set(user),业务代码里直接UserContext.get().getId(),请求结束在拦截器的afterCompletion里调用UserContext.clear()防止线程池复用导致数据串号。
跨域问题开发阶段就碰到了。前后端分离后,前端跑在http://localhost:8081,后端跑在http://localhost:8080,浏览器会拦截跨域请求。我写了CORS配置类,允许指定来源访问,并放行OPTIONS预检请求。到了生产环境,前端和后端用Nginx反代到同一个域名下,通过路径区分,跨域问题自然消失,这是后话了。
4.2 MyBatis动态SQL实现多条件检索
馆藏检索是系统的核心能力,也是MyBatis动态SQL最典型的应用场景。前端藏品列表页有一排筛选条件:编号、名称、朝代、分类、材质、状态。用户填几个就查几个,一个都不填就是全量分页查询。
Mapper XML里我这么写核心的检索SQL:
<select id="selectCollectionList" resultType="com.example.entity.Collection"> select * from t_collection <where> <if test="collectionNo != null and collectionNo != ''"> and collection_no like concat('%', #{collectionNo}, '%') </if> <if test="name != null and name != ''"> and name like concat('%', #{name}, '%') </if> <if test="dynasty != null and dynasty != ''"> and dynasty = #{dynasty} </if> <if test="categoryId != null"> and category_id = #{categoryId} </if> <if test="status != null"> and status = #{status} </if> </where> order by create_time desc </select><where>标签会自动处理掉第一个条件前面的and,这是最省心的写法。所有参数值都用#{}预编译传参,可以防SQL注入。注意这里我在做模糊查询时没用${}拼%,而是用MySQL的concat函数拼,这样既安全又兼容。
分页我用了PageHelper插件。用法很简单,查询前写一句PageHelper.startPage(pageNum, pageSize),紧接着的查询会自动拼接limit语句,返回的PageInfo里直接带着总条数和页数。注意这个静态方法只对下一句查询生效,如果中间插了别的查询,分页就错位了,这是一个很容易踩的坑。
很多面试题爱问MyBatis一级缓存二级缓存,实际项目里我反而默认关掉了二级缓存。馆藏数据实时性要求高,管理员刚改了一条记录,立刻查到旧数据没法接受。而一级缓存作用范围只在同一个SqlSession里,Spring管理下每次请求都是新会话,基本用不上。SQL性能靠索引解决,缓存不填这个乱。
4.3 借展出入库的事务处理
借展模块涉及两张表的联动操作:新增一条借展记录,同时把藏品状态改成"外借"。如果先插记录再改状态,中间任何一步失败,都会留下中间状态的数据。
这里必须加@Transactional事务注解。我把借出逻辑写成两个步骤:先插入t_lend_record,再更新t_collection的status字段为0。事务保证要么两步全成功,要么全回滚。更稳妥的做法是接口里先校验藏品当前状态——只有"在库"的才能借出,否则直接抛业务异常。
归还是反向操作:更新借展记录的back_date为当前日期、status改为"已归还",再把藏品状态改回1。归还时还要判断是否逾期,逾期就在remark里加一条提示。这些逻辑放在LendService里,接口只做参数校验,业务复杂度都收敛在Service层。
回滚有一个坑必须注意:@Transactional默认只在遇到RuntimeException时回滚,如果方法里捕获了异常没抛出去,事务是不会回滚的。所以Service里处理业务异常要么直接抛出,要么手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(),否则数据就悄悄错了。
5. Vue前端实现:列表、表单、图片上传与路由守卫
5.1 工程初始化与axios封装
前端我用vue create初始化项目,选上Vue Router和Vuex这两个插件。装依赖这里有个容易卡住的点:ElementUI的安装命令要加-S,不然组件引入了但不在运行依赖里,打包后样式全丢。
axios封装成src/utils/request.js,这段代码建议直接抄:
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code === 401) { localStorage.removeItem('token') router.push('/login') return Promise.reject(new Error('未登录')) } if (res.code !== 200) { Message.error(res.message) return Promise.reject(new Error(res.message)) } return res }, error => { Message.error(error.message) return Promise.reject(error) } ) export default requestbaseURL设为/api是有讲究的。开发环境用vue.config.js里的devServer.proxy把/api转发到后端地址,生产环境用Nginx做一样的转发。前端代码里始终只写相对路径,切换环境不用改代码,只需要改代理配置。这是前后端分离项目里很实用的约定。
5.2 藏品管理页面的核心交互
藏品列表页的交互模式是标准的三段式:顶部搜索区、中间表格区、底部弹窗表单。搜索区的每个筛选控件绑定一个data属性,点"查询"按钮时把这些参数传给后端接口,表格重新加载。
表格列里重点是cover_image字段的处理。图片不一定每件藏品都有,我用el-image组件,同时设置fit="cover"和插槽里的占位图。点击缩略图可以放大预览,这是ElementUI自带功能,preview-src-list传图片地址数组就生效。
弹窗表单里的图片上传是单独处理的。el-upload设置action="/api/collection/upload",上传成功后端返回图片的相对路径,前端把路径存进表单的coverImage字段。提交表单时图片路径和文字信息一起发给后端,入库时写入t_collection表。
表单校验规则里,编号是必填且要求格式为字母+数字组合,用正则/^[A-Za-z]{2}\d{5}$/校验。名称、朝代必填,重量必须是数字。ElementUI的rules属性加上prop绑定就能实现提交时自动校验,不通过不会发请求。
新增和编辑用了同一个弹窗组件,区别只是初始数据是否为null。打开新增时清空表单,打开编辑时用Object.assign回填数据。编辑提交时把id一起传过去,后端根据id判断是插入还是更新。
5.3 路由守卫和权限控制
权限控制的实现分两层。第一层是路由级守卫,在router.beforeEach里判断token是否存在,不存在就重定向到登录页。第二层是角色控制,在路由的meta里定义roles数组,比如:
{ path: '/collection/edit', component: CollectionEdit, meta: { roles: ['admin', 'editor'] } }守卫里取到当前用户的角色,没有匹配就跳去403页面。游客角色能访问列表页和详情页,但访问编辑页会被拦下来。
按钮级的控制用自定义指令或v-if。我在列表页给"新增""编辑""删除"按钮加了v-if="isAdminOrEditor()"的判断,这个方法是根据后端返回的用户角色做布尔判断。有同学图省事只在前端藏起按钮,其实做法不完整——后端的拦截器也必须校验接口权限,前端隐藏只是体验优化,后端拦截才是安全底线。
6. 部署上线完整教程:从本地jar包到Nginx反向代理
6.1 MySQL初始化与环境检查
部署前我先在服务器上装好三样东西:JDK 8、MySQL 8.0、Nginx。JDK版本建议用8或11,除非有特殊需求不要上更高,原因后面专门讲。检查版本命令记一下:
java -version mysql --version nginx -v数据库初始化的完整流程是这样的:把项目里的collection.sql上传到服务器,执行mysql -u root -p < collection.sql导入。SQL文件开头要有CREATE DATABASE IF NOT EXISTS springboot_collection DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,然后是USE语句,这样一条命令就能把库表结构全部导好。
导入后验证一下:mysql -u root -p进控制台,SHOW TABLES;看到六张表就算成功。再执行SELECT COUNT(*) FROM t_collection;确认表里有没有初始数据。建议SQL文件里面预置一个管理员账号,用户名admin、密码在SQL里用BCrypt哈希值写死,首次部署直接能登录。
6.2 后端jar打包与启动
后端打包前必须改好application.yml,这里是最容易出错的地方。主要配置如下:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/springboot_collection?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password servlet: multipart: max-file-size: 10MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true upload: path: /data/collection/uploads/serverTimezone=Asia/Shanghai必须加,不加的话连接MySQL 8.0会报时区错误。map-underscore-to-camel-case: true让数据库的create_time字段自动映射成Java的createTime,这是我强烈建议开的配置,省掉一大堆手动映射。上传路径我配了服务器上的绝对路径/data/collection/uploads/,这个目录要提前创建好,否则上传图片时报FileNotFoundException。
打包命令很简单:
mvn clean package -DskipTests打包完成后target目录下会生成一个collection-system.jar。启动之前先确认端口没被占用,然后:
nohup java -jar /opt/collection/collection-system.jar > /data/logs/collection.log 2>&1 &nohup加&让程序在后台运行,日志重定向到文件里方便排查。启动后看一眼日志,出现"Started Application"就说明起来了。
这里不建议用java -jar直接跑,一关终端进程就死了。如果服务器有systemd,更规范的做法是写一个unit文件托管服务,开机自启、异常重启都能自动处理,项目级别推荐。
6.3 前端打包与Nginx配置
前端打包之前,先把vue.config.js里的devServer.proxy确认好——这个只在本地开发用,打包后的代码不会包含这个代理配置。然后执行:
npm run build生成的dist目录是整个静态站点。上传到服务器的/var/www/collection/目录,接下来配置Nginx。
Nginx配置是整个部署环节的关键,我贴一份能直接用的:
server { listen 80; server_name your_domain_or_ip; root /var/www/collection/dist; index index.html; location / { try_files $uri $uri/ /index.html; } 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; } location /uploads/ { alias /data/collection/uploads/; } }location /里的try_files可不是随便写的。Vue Router如果用了history模式,前端路由在刷新时会向后端请求真实的URL路径,比如/collection/1,服务器上根本没有这个文件,不配置try_files就会404。它的作用是:请求路径匹配不到真实文件时,统一回退到index.html,由Vue Router接管页面。
location /api/把接口请求转发到后端的8080端口,这样浏览器里所有请求都走80端口,不存在跨域问题。/uploads/映射到图片上传目录,否则前端<img>标签访问封面图片会403。
配置改完后:
nginx -t nginx -s reloadnginx -t检查语法,通过后再reload,千万不要不检查直接reload,写错一个分号整个服务就挂了。
6.4 部署后的联通检查
部署完成后按这个顺序检查,基本能定位所有问题:
- 先看后端日志有没有报错:
tail -f /data/logs/collection.log - 直接在后端服务器本地curl接口:
curl http://127.0.0.1:8080/api/collection/list?pageNum=1&pageSize=10,通了说明后端OK - 浏览器访问
http://服务器IP,能看到首页说明静态文件OK - 登录一次,看Nginx的
access.log,确认/api/请求有没有到后端 - 上传一张测试图片,再用浏览器打开返回的图片URL,确认
/uploads/映射成功
这套检查顺序的价值在于逐层缩小故障范围。后端没起来就不需要去看前端配置,静态文件访问正常再怀疑接口转发,一步一步来,半小时以内肯定能定位问题。
7. 开发中踩过的坑与排查思路
7.1 刷新页面404:前端路由history模式的经典坑
这个问题出现的时机是第一次部署上线,登录后随便点进一个藏品详情页,按F5刷新,页面直接白屏加404。原因前面提过,history模式下浏览器把URL路径当成真实资源去请求了。
排查思路其实很简单。打开Nginx错误日志,看到一堆open() "/var/www/collection/dist/collection/1" failed (2: No such file or directory),基本就是这个问题。当时第一反应是想在Vue Router里加base配置,折腾半天没用。后来查资料才发现标准解法就是try_files $uri $uri/ /index.html;。
这也解释了为什么开发环境一直没发现这个bug——Vue的devServer默认配置了history fallback,任何路径都返回index.html,问题被隐藏了,只有部署到Nginx才会暴露。
7.2 图片上传成功却访问不了
第二个印象深刻的坑是图片上传。本地开发时图片上传后能正常显示,部署到服务器后,上传接口返回200,但前端访问图片URL一直是404。
逐层排查后发现,问题出在上传路径上。本地开发时我配置的相对路径,图片被存进了后端jar包所在目录的临时子目录里。服务器上每次重新部署都会覆盖临时目录,重启后图片就全丢了。
修复方案是配置外部独立目录:upload.path: /data/collection/uploads/,然后在Nginx加location /uploads/映射。再通过一个接口把数据库里的相对路径统一处理,返回完整的URL前缀。这里特别提醒:图片永远不要以二进制存入MySQL的Blob字段。数据库体积膨胀后备份和查询都会变慢,而且图片这种大字段和结构化数据混在一起,性能影响非常明显。文件存磁盘,数据库只存路径,是最标准的做法。
7.3 版本选择的经验:为什么没有追最新的SpringBoot 3
最后聊一下版本选择。现在网上很多新项目直接上SpringBoot 3.x,我也试过,但最终还是把主力版本定在SpringBoot 2.7.x。原因有三点。
第一是JDK版本门槛。SpringBoot 3要求JDK 17以上,不少生产服务器还停留在JDK 8,升级JDK本身就是一个有风险的动作,涉及老项目兼容、运维脚本调整,不是改个JAVA_HOME就完事。
第二是依赖生态。SpringBoot 3把javax.*换成了jakarta.*,MyBatis的starter和相关插件如果没跟上,就会出现ClassNotFoundException。我刚上手时用的一个分页插件在SpringBoot 3下直接不能启动,换成2.7.x马上就好。对业务项目来说,稳定压倒一切。
第三是MyBatis官方对SpringBoot 3的适配也是逐步完善的,网上相关资料相对少。遇到问题搜出来的答案大部分还是2.x时代的做法,参考价值会打折扣。
这不是说SpringBoot 3不好,而是做项目要分清"追新技术"和"用成熟技术把业务落地"的区别。馆藏系统这种偏传统的数据管理项目,数据准确和运行稳定比技术栈版本新重要得多。如果做全新学习项目,SpringBoot 3完全没问题;如果是给客户交付系统,选2.7.x配合成熟的MyBatis生态,后期维护会省心很多。这套SpringBoot+Vue+MyBatis+MySQL的组合,恰好就是在"够用、稳定、资料全"这几个维度上达到了很好的平衡。