☰
SpringBoot2+Vue3+MyBatis-Plus实战:红色革命文物征集管理系统开发全解析
2026/10/5 15:31:34 网站建设 项目流程

做文物征集管理系统这个项目的时候,我印象最深的一点是:技术栈看起来都是老朋友,SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0,但真正把这一整套串起来做业务落地,还是有不少值得记录的东西。这个红色革命文物征集管理系统,是一个典型的Java Web MVC模式前后端分离项目,核心是解决文物征集从线索登记、专家鉴定到入藏管理的全流程问题。我打算把这套系统的设计思路、核心表结构、关键代码、前端实现,还有我实操中踩过的坑,完整地拆开讲一遍。不管你是准备做类似的文物管理、档案管理、展品管理系统,还是单纯想看看SpringBoot2加Vue3加MyBatis-Plus这套组合在真实项目里怎么配合,这篇文章都值得你花几分钟读完。

1. 项目定位与整体架构设计思路

1.1 为什么是“文物征集管理”这个业务

先说说业务背景。很多纪念馆、博物馆每年会有大量的革命文物征集需求,来源包括社会无偿捐赠、老战士后代移交、定向征集采购等等。以前这些工作大多靠纸质登记、Excel表格流转,文物一到库房,信息就断在了某个人的电脑里。这个系统要解决的就是三件事:征集线索不丢失、鉴定流程可追溯、文物档案统一管。

围绕这三个核心诉求,我把系统拆成了几个业务模块:文物信息管理、征集线索管理、鉴定评估管理、入藏登记管理、展览提用管理,再加上用户权限和统计分析。模块划分的核心原则是跟着业务流程走,而不是跟着数据表走。每个模块对应征集流程中的一个关键环节,这样后期加功能、加状态都方便。

1.2 MVC模式在前后端分离架构下的落地方式

项目名里带“MVC模式”,很多刚入门的朋友容易误解,以为用了SpringBoot就自动是MVC了。实际上在前后端分离架构下,MVC的职责边界发生了明显变化。传统的JSP时代,Controller既要接收请求又要准备视图,ModelAndView里既带数据又带页面跳转逻辑。而这个项目是SpringBoot提供纯RESTful API,不再返回视图,前端完全交给Vue3渲染。

落地的时候,我后端将代码分成Controller、Service、Mapper三层,对应表现层、业务层、数据访问层。Controller只做参数接收和结果封装,不写任何业务逻辑;Service层负责事务和业务流程编排;Mapper层用MyBatis-Plus操作数据库。前端Vue3这边,组件相当于View,Pinia或Vuex的Store相当于Model,路由和事件负责Controller的调度作用。前后端分离下的MVC,更像是一种松耦合的分工约定,而不是代码层面的强制约束。

1.3 整体模块划分与功能清单

系统功能清单在开发前就要列清楚,不然后期甲方今天加一个字段、明天加一个报表,代码会被改散架。我最终确定的核心功能如下:

  • 系统管理:用户管理、角色管理、菜单权限、操作日志。
  • 文物征集:征集公告管理、线索登记、来源信息录入。
  • 文物档案:文物基础信息、照片上传、年代材质尺寸等属性维护。
  • 鉴定管理:专家鉴定任务分配、鉴定意见录入、鉴定结果审核。
  • 入藏管理:入库登记、库房位置、保管人、出入库记录。
  • 展览提用:展品出库、归还登记、状态跟踪。

这套功能列表看起来简单,但每一个模块背后都对应着一套状态流转和权限约束。比如一个文物从“征集线索”变成“待初审”,再变成“鉴定中”“已入藏”,每一步的字段可编辑范围都不同。这个设计是我觉得整个项目里最核心、也最容易被新手忽略的部分。

2. 核心技术栈选型与版本搭配

2.1 SpringBoot2还是SpringBoot3

项目标题定的是SpringBoot2,说实话现在新项目用SpringBoot3的也不少,但SpringBoot2在2025年仍然有大量存量系统和学习资料,尤其是一些依赖老版本JDK的项目,2.x还是最稳妥的选择。我用的是SpringBoot2.7.x,对应JDK1.8,这套搭配对MyBatis-Plus的兼容性也是最好的。

选SpringBoot2而不是3,还有一层考虑:很多第三方生态组件,比如代码生成器、工作流引擎、某些报表组件,在SpringBoot3刚出来那阵子适配还不完善。SpringBoot2遇到问题时,无论是查Stack Overflow还是翻GitHub的Issue,成熟的解决方案都更多。对于做管理系统这种业务型项目,稳定压倒一切,没必要追新。

2.2 Vue3组合式API为什么比选项式API更适合

前端这块我选了Vue3配合Vite构建,用组合式API(Composition API)而不是旧版的选项式API。原因很实际:管理系统页面里,列表页逻辑高度相似,如果每写一个页面都重复data、methods、watch那一套选项式结构,代码复用非常麻烦。

用组合式API之后,我可以把“分页查询”“表单弹窗”“状态切换”这些逻辑抽成独立的函数,每个页面按需引用。比如useArtifactTable这个函数,封装了列表加载、搜索条件组装、分页参数维护,在文物列表页和线索列表页都能直接复用。这套组合式API在代码组织和逻辑复用上的优势,写过一个真实项目之后感受才深,光看文档是体会不到的。

2.3 MyBatis-Plus的通用CRUD能力边界

MyBatis-Plus在这个项目里承担了大部分基础数据访问工作。它的BaseMapper内置了insert、deleteById、selectById、updateById、selectPage这些通用方法,配合LambdaQueryWrapper,基本不需要手写SQL就能覆盖80%的增删改查场景。

但要清醒认识它的能力边界,MyBatis-Plus不是万能的。像多表关联查询、复杂统计报表、动态条件特别多的搜索场景,我依然选择手写XML里的自定义SQL。另外,分页插件是独立配置的,不配置PaginationInnerInterceptor的话,selectPage根本不生效,这是我见过很多人踩的第一个坑。

2.4 MySQL8.0的选型要点

数据库用MySQL8.0,最大的好处是默认字符集utf8mb4,emoji和生僻字都能正常存储,文物名称里经常出现的“鎏金”“珐琅”这类生僻字,放到老版本utf8里就得折腾半天。另一个8.0带来的改善是窗口函数和公共表表达式(WITH子句),做排名统计、递归查菜单树的时候方便很多。

在MySQL8.0下我做了两件重要配置:一是数据库驱动用com.mysql.cj.jdbc.Driver,这是8.0开始的新驱动类名;二是连接URL要带serverTimezone=Asia/Shanghai,否则默认时区不对,Java里LocalDateTime和数据库时间会差8个小时。这两处虽然在SpringBoot2的自动配置下不一定会报错,但数据查出来就是不对,属于典型的隐性坑。

3. 数据库设计与核心业务流转

3.1 核心表结构设计

数据库设计是整系统的基础,表结构如果设计得不合理,后面写代码的时候每写一个功能都难受。这个系统的核心表我分成三组:用户权限组、文物档案组、流程记录组。

用户权限组用经典的sys_user、sys_role、sys_menu、sys_user_role四张表。文物档案组以artifact表为中心,关联artifact_image存放多张图片。流程记录组包括appraisal_record鉴定记录、storage_record入藏记录、exhibit_record展览记录。这样的分组逻辑是:用户权限表和业务数据表分开,业务数据里主表与明细表分开,流程记录各自独立,互不干扰。

文物主表的关键字段我列举一下:

CREATE TABLE artifact ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', artifact_no VARCHAR(50) NOT NULL UNIQUE COMMENT '文物征集编号', name VARCHAR(200) NOT NULL COMMENT '文物名称', category VARCHAR(50) COMMENT '文物类别', dynasty VARCHAR(100) COMMENT '所属年代', material VARCHAR(100) COMMENT '材质', size_desc VARCHAR(200) COMMENT '尺寸描述', source_type TINYINT COMMENT '来源类型:1捐赠 2移交 3征集 4收购', source_person VARCHAR(100) COMMENT '来源单位/个人', contact_phone VARCHAR(30) COMMENT '联系电话', description TEXT COMMENT '文物描述', status TINYINT DEFAULT 0 COMMENT '状态:0待初审 1初审通过 2鉴定中 3待入藏 4已入藏 5已退回', create_by VARCHAR(50) COMMENT '创建人', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='革命文物信息表';

这个表设计的用心之处,在于把状态和业务属性拆开。status只负责流转,而具体到哪个环节产生了什么数据,由各个记录表去承载。比如鉴定环节通过了,appraisal_record里就有一条result为通过的数据,artifact.status才相应更新。这样每一步操作都有留痕,后期审计或者追溯不会扯皮。

3.2 文物征集业务的状态流转设计

状态流转是整个系统最核心的业务逻辑。我把文物从线索到入藏分成六个状态:待初审、初审通过、鉴定中、待入藏、已入藏、已退回。每个状态对应的操作权限不同,比如“鉴定中”状态下,只有鉴定专家角色的用户能录入意见,普通工作人员只有只读权限。

状态流转我设计成了单向加回退的模式,正常情况下不允许跳状态。退回操作单独处理,退回后文物状态回到待初审,同时保留原鉴定记录。这样设计的好处是流程清晰、每个节点都可以输出统计报表。比如统计“本月鉴定通过率”,直接从appraisal_record表里按result分组汇总就行,不需要在Java代码里做复杂的状态机判断。

3.3 文件存储与图片处理方案

文物照片是必须的,而且每个文物可能有多张照片,包括正面、局部特写、铭文细节等。我设计了artifact_image表,一对多关联文物主表。早期的方案是把图片以Base64字符串存在数据库里,后来发现打个包就膨胀到几十MB,果断放弃了。最终用的是本地磁盘存储加数据库存路径的方案,Nginx映射静态目录访问。

图片上传时做了两个限制:单张不超过5MB,支持jpg、png、webp格式。上传接口返回图片ID和访问URL,前端拿到URL后拼接成img标签直接预览。这里有个小坑,SpringBoot上传文件默认单文件1MB、单次请求10MB,不配置的话超过大小直接报错。我在配置类里调成了单文件20MB、请求100MB,同时做了图片压缩,大图先压到宽度1920以下再上传,避免浏览器加载卡顿。

4. 后端接口实现与关键代码拆解

4.1 分层结构与统一返回

后端我严格走了Controller-Service-Mapper三层结构,Controller里不写业务逻辑。每个接口的返回结构统一封装成Result对象,包含code、msg、data三个字段。这样的好处是前端axios拦截器里只需要判断一次code,不用每个接口单独处理错误。

Result统一返回的实现其实很简洁:

@Data public class Result<T> { private Integer code; private String msg; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMsg("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String msg) { Result<T> result = new Result<>(); result.setCode(500); result.setMsg(msg); return result; } }

在Controller里所有接口都返回Result,配合全局异常处理器@RestControllerAdvice,把参数校验异常、业务异常、系统异常统一转成Result格式。这套结构老练但好用,接口文档看起来也整齐。

4.2 通用CRUD服务的实现

MyBatis-Plus的ServiceImpl和IService提供了基本的增删改查封装,但管理系统的列表查询往往不只是简单的selectList,要带分页、带条件、按时间排序。我用LambdaQueryWrapper来动态组装查询条件,既安全又能避免字符串拼接SQL注入。

核心查询代码如下:

@Override public Page<Artifact> queryArtifactPage(ArtifactQuery query) { LambdaQueryWrapper<Artifact> wrapper = new LambdaQueryWrapper<>(); // 按名称模糊查询 wrapper.like(StringUtils.isNotBlank(query.getName()), Artifact::getName, query.getName()); // 按类别精确查询 wrapper.eq(StringUtils.isNotBlank(query.getCategory()), Artifact::getCategory, query.getCategory()); // 按状态查询 wrapper.eq(query.getStatus() != null, Artifact::getStatus, query.getStatus()); // 按时间范围查询 wrapper.between(query.getStartTime() != null && query.getEndTime() != null, Artifact::getCreateTime, query.getStartTime(), query.getEndTime()); // 按创建时间倒序 wrapper.orderByDesc(Artifact::getCreateTime); return this.page(new Page<>(query.getPageNum(), query.getPageSize()), wrapper); }

这里有几个细节值得注意。LambdaQueryWrapper方法重载里第一个参数是boolean condition,条件为false时整个条件自动不拼接。这个特性让代码看起来像一堆if判断,实际上不会产生多余的SQL片段。还有排序字段不要用用户传入的字符串做orderBy,防止SQL注入,写死字段才是安全的。

4.3 文件上传与导入导出

文件上传接口我单独写了一个FileController,接收MultipartFile参数,校验文件类型和大小,然后生成UUID文件名保存到指定目录。保存路径按日期分目录,比如upload/2025/06/,避免单个目录文件过多影响读取性能。

Excel导入导出在征集线索批量录入时很有用。我用的是EasyExcel,定义好VO类,加@ExcelProperty注解,一行代码就能导出列表。导入的时候要注意模板校验,比如“来源类型”字段只有四种取值,用户随便填了个“其他”就得拦截并提示具体行号。我在导入逻辑里加了逐行校验,把错误信息收集成List,全部校验完再批量插入,有错误就整体回滚,绝不部分成功部分失败。

4.4 登录认证与权限控制

认证和权限这个项目用的是JWT加拦截器方案。登录接口校验用户名密码之后生成token返回前端,前端存储在localStorage里,每次请求在Authorization请求头上携带。后端写了一个JwtInterceptor拦截器,校验token有效性和过期时间,把用户信息解析出来放入ThreadLocal,方便Service层获取当前登录人。

权限控制这块,我在JwtInterceptor里加了角色判断。比如鉴定意见录入接口只有ROLE_EXPERT角色能访问,通过注解@RequireRole("expert")实现。做法其实不复杂,就是拦截器里先解析出用户角色,再检查接口注解上声明的角色列表。推荐的做法是直接集成Spring Security加Sa-Token,但这个小项目用轻量拦截器也能满足要求,代码量还更少。

5. Vue3前端实现重点

5.1 Vite项目搭建与工程配置

前端是标准Vue3加Vite项目,通过npm create vite@latest命令创建。工程化配置了几个核心依赖:vue-router做路由,pinia做状态管理,axios做HTTP请求,element-plus做UI组件,vite-plugin-mock在开发阶段模拟接口数据用。

Vite相比Webpack最大的体验提升是启动速度,大型管理系统动辄几十个路由,Webpack冷启动要三十秒,Vite基本三秒内就能起来。热更新也快,改一个组件保存后浏览器立即刷新,不用像以前那样等Webpack增量编译。配置代理解决开发跨域也很方便,在vite.config.ts里配server.proxy,把/api开头的请求转发到后端地址。

5.2 基于组合式API的后台页面开发

后台管理页面高度重复,无非是“搜索栏+表格+分页+弹窗表单”的结构。我用组合式API封装了一个通用的usePage钩子,把分页、查询、加载这些逻辑收拢起来。每个页面组件里只要调用这个钩子,传入对应的数据接口和搜索字段配置,就能快速生成列表逻辑。

举一个文物列表页的核心代码:

<script setup> import { reactive, ref, onMounted } from 'vue' import { getArtifactList, deleteArtifact } from '@/api/artifact' import { ElMessage, ElMessageBox } from 'element-plus' const loading = ref(false) const tableData = ref([]) const total = ref(0) const queryParams = reactive({ name: '', category: '', status: null, pageNum: 1, pageSize: 10 }) const loadData = async () => { loading.value = true try { const res = await getArtifactList(queryParams) tableData.value = res.data.records total.value = res.data.total } finally { loading.value = false } } const handleSearch = () => { queryParams.pageNum = 1 loadData() } const handleDelete = (row) => { ElMessageBox.confirm('确定删除该文物档案吗?', '提示', { type: 'warning' }) .then(async () => { await deleteArtifact(row.id) ElMessage.success('删除成功') loadData() }) .catch(() => {}) } onMounted(loadData) </script>

form表单弹窗用v-model控制显示,子组件接收一个row prop用于编辑回显,通过defineExpose暴露formRef和validate方法,父组件通过ref调用子组件方法。vue3里组件通信的细节比较碎,但理清props、emit、defineExpose这几条线之后,开发效率并不低。

5.3 与后端对接中的几个细节

前后端对接是项目里最容易出问题的环节。我先定义了axios实例,baseURL设置为/api,请求拦截器里统一从localStorage取token拼到Authorization头,响应拦截器里判断code是否为200,不是就弹错误提示并拦截。这样业务代码里不需要每处写try-catch和错误提示,清爽很多。

还有一个细节是日期格式。后端返回LocalDateTime默认序列化成“2025-06-18T10:30:00”这种格式,跟国内用户习惯不一致。我在后端配置了Jackson的日期格式化,全局把LocalDateTime序列化成“yyyy-MM-dd HH:mm:ss”格式,前端表格直接显示,不需要额外处理。

6. 部署、测试与常见问题排查

6.1 本地环境搭建与联调

本地开发环境我用了Docker起MySQL8.0,命令比较简单:

docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=root123456 \ -e MYSQL_DATABASE=artifact_system \ -v /data/mysql8:/var/lib/mysql \ mysql:8.0

这样不会把本机环境搞乱,MySQL升级、换版本直接重新起容器就行。数据目录挂载到宿主机,容器删了数据还在。后端启动前要确认application.yml里的数据库连接、Redis连接(如果用了)都指向本地对应端口。前后端联调的基本流程:先启动MySQL容器,导入初始化SQL,再启动SpringBoot后端,最后npm run dev启动前端。

6.2 常见报错与解决记录

我挑4个实际遇到且高频的问题记录一下,这4个问题在我带过的几个项目里反复出现:

报错现象根本原因解决方案
分页查询返回所有数据,total为0没有配置MyBatis-Plus分页插件在配置类中注入PaginationInnerInterceptor
插入数据时中文乱码MySQL连接URL没指定characterEncodingURL加useUnicode=true&characterEncoding=utf8
LocalDateTime反序列化报错Jackson没有JavaTimeModule引入jackson-datatype-jsr310并配置Localtime格式化
Vue打包后访问404路由是history模式,服务器没做重定向后端配置前端history fallback到index.html

第一个分页问题我想多说一句,MyBatis-Plus的分页插件配置网上教程一堆,但很多人抄了代码却忘了@Configuration注解,导致类没被扫描,插件根本没生效。还有一个细节是插件放到最后拦截还是最前拦截,官方文档建议按顺序最后添加,实测中位置不对偶尔会跟自定义拦截器起冲突。

6.3 性能与安全的细节优化

管理系统面向内部用户,并发量不大,不需要引入Redis做缓存来撑高并发,但有些地方还是要做性能优化。我在文物列表页加了后端条件分页,前端搜索时始终走接口而不是一次性拉全量数据前端过滤。图片走Nginx静态文件服务,不经过Java应用层,Java应用只处理接口请求,体验提升很明显。

安全方面做了几件常规但必要的事:JWT过期时间设为2小时,前端路由守卫拦截未登录跳转登录页,后端对无token的请求统一返回401状态码;MySQL连接使用最小权限账号,不直接用root;上传文件时校验扩展名和ContentType,防止上传恶意脚本。这些谈不上多高深,但管理系统面向真实业务场景,安全意识和细节不能缺。

最后分享一个我实际操作中的心得:这类管理系统的开发,最消耗精力的不是技术难点,而是业务状态不清晰时反复改表结构。我在项目初期花了整整一天跟使用方确认每个业务节点的角色和操作权限,后面写代码、改需求都顺畅很多。做系统之前,把状态流转图画明白,比讨论用SpringBoot3还是2、用组合式API还是选项式API,重要得多。另外,如果你打算在这个项目基础上扩展,可以先从移动端适配的H5版文物展示页做起,或者加一个基于统计报表的数据大屏,这两个方向都跟现有数据结构衔接得比较自然。

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

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

立即咨询