博物馆、档案馆、文化馆这类单位的信息化项目,我做下来觉得最核心的问题不是技术本身,而是“台账怎么电子化才不乱”。这里分享一个基于 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 构建的线上历史馆藏系统,整套源码我近期整理过,也配了完整文档,正好可以拿来讲讲一个馆藏类管理系统从前到后是怎么落地成型的。
这套系统的核心目标很朴素:把线下纸质文物账本变成可检索、可追溯、可权限管控的电子台账,同时覆盖藏品登记、分类管理、库房派存、借展记录、修复记录和用户权限管理这些日常业务。适合准备做全栈项目练手的人、正在做相关毕设的同学,以及单位内部确实需要一套轻量级藏品管理后台的开发者。下面我把这个项目从技术选型、数据库设计、后端实现、前端联调,到部署运维的完整过程拆开说,中间穿插不少实际踩过的坑,照着思路走能少走很多弯路。
1. 项目整体设计与技术选型思路
1.1 馆藏系统的核心需求拆解
先别急着写代码,拿到“历史馆藏系统”这个需求时,第一件要做的事是把业务模型看清楚。馆藏系统不像电商那样订单满天飞,它的数据特点是“资产台账 + 流转记录”,一件藏品从入馆登记开始,要经历编目、定级、排架、借展、修复、盘点等一系列动作。每个动作都会改变藏品的状态,而所有状态变化都必须留痕。
我在设计时把需求拆成了三层:最底层是基础的档案数据,也就是藏品主表;中间层是业务动作数据,比如借展记录、修复记录、出入库流水;最上层是系统管理数据,包括用户、角色、菜单这些权限相关的表。这套分法和大多数后台管理系统是一致的,但馆藏系统有个特殊点:藏品的分类体系和存放位置结构必须单独建模,不能简单塞一个字段。因为实际操作中你不但要回答“库里有一件唐代的瓷器”,还得回答“它存放在3号库房第2排第5格”,这种层级化的查询需求直接影响数据库设计。
确定好业务模型之后,再把非功能性需求列出来,比如操作日志、数据备份、附件上传。这些模块看似不起眼,但上线之后使用频率极高。我见过不少项目把附件路径直接存在业务表里,结果后期文件一多根本没法管理,所以这个项目里我把文件上传单独拆了一张 file 表,业务表只关联 fileId,这套做法值得保留。
1.2 技术栈选型背后的考量
选型不是越新越好,而是看团队熟悉度和项目复杂度。这个项目技术栈一共四样:后端 SpringBoot2,前端 Vue3,持久层 MyBatis-Plus,数据库 MySQL8.0,我把选型理由和对应场景整理成了表:
| 技术栈 | 选择理由 | 使用场景 |
|---|---|---|
| SpringBoot2 | 自动配置完善,生态成熟,中小型系统开发效率高 | 后端接口服务、定时任务、文件处理 |
| MyBatis-Plus | 通用CRUD + 条件构造器,大幅减少手写SQL量,复杂查询仍可控 | 单表操作、批量写入、分页查询 |
| Vue3 | Composition API 逻辑复用方便,配合 Vite 构建效率高 | 后台管理页面、表单交互、数据可视化 |
| MySQL8.0 | utf8mb4 字符集、窗口函数、JSON 字段支持,应对馆藏数据绰绰有余 | 核心业务表存储、检索统计 |
有人可能问为什么不用 SpringCloud 微服务,或者为什么不选 JPA。答案是:馆藏系统的并发量和使用规模,单体应用完全扛得住,微服务反而会把架构复杂度拉高。MyBatis-Plus 对比 JPA 的最大优势在于,馆藏系统这种需要频繁做“多条件组合查询”的场景,用 LambdaQueryWrapper 写条件比 JPA 的 Specification 更直观,而且团队里已经有不少 SQL 老手,MyBatis-Plus 保留的 SQL 自由度对排障也有帮助。Vue3 这块也没什么悬念,后台管理系统是 Composition API 最典型的应用场景,逻辑复用干净,代码组织清晰。MySQL8.0 则是实打实的数据底座,后面第三章我会展开讲它的表结构设计和优化点。
1.3 项目工程结构怎么划分
选型定了之后,工程结构直接决定代码能不能长期维护。我没有用那种包名乱飞的分层方式,而是在单体目录下做了模块化。后端大致是 controller、service、mapper、entity、config、common 这几个主包,common 下面再放统一的返回体、异常处理、常量工具。这么做的好处是:新增一个功能模块时,你在每个包下各加一个类即可,新人接手也能很快找到对应文件。
前端这边我用 Vue3 + Vite 搭建,目录按 views、components、api、router、store、utils 划分。views 下面按业务模块建子目录,比如 collection、exhibition、system;api 目录里每个文件对应一个后端模块的接口方法。组件层面只抽公共组件,业务组件就放在对应页面里,避免过度抽象导致页面跳转都找不到组件在哪。
工程结构确定后,我把整个项目的设计文档和数据库脚本同步整理出来,也就是标题里提到的“含文档”。这个动作相当重要,因为源码只能表达“怎么实现”,文档才能表达“为什么这么实现”。后面第六章我会专门讲讲文档怎么写才对接手的人有价值。
2. 数据库设计:MySQL8.0 下的馆藏核心模型
2.1 藏品主表和扩展字段的取舍
数据库是一个管理系统的地基,我已经反复吃过设计不合理的亏。馆藏系统的核心表是藏品主表 collection,它的字段设计要同时满足固定属性和灵活扩展的需求。固定属性包括藏品编号、名称、年代、质地、级别、来源方式、当前状态等;但馆藏单位不同,藏品属性差别挺大,有的单位还要记录完残程度、入馆凭证号、保险估值之类。如果把这些全做成字段,表结构会被撑得很难看。
MySQL8.0 的 JSON 类型在这时候就派上用场了。我在 collection 表里留了一个 extra_json 字段,用来存各单位自定义的扩展属性;同时用 entity 表里固定的核心字段保证高频查询和列表展示的性能。实际操作中,核心字段建普通索引,扩展属性只做详情展示,不参与主查询条件,这样既灵活又不至于索引失效。给个参考建表片段:
CREATE TABLE `collection` ( `id` bigint NOT NULL AUTO_INCREMENT, `code` varchar(64) NOT NULL COMMENT '藏品编号', `name` varchar(256) NOT NULL COMMENT '藏品名称', `era` varchar(64) DEFAULT NULL COMMENT '年代', `material` varchar(64) DEFAULT NULL COMMENT '质地/材质', `level` tinyint DEFAULT NULL COMMENT '珍贵级别', `category_id` bigint DEFAULT NULL COMMENT '分类ID', `room_id` bigint DEFAULT NULL COMMENT '库房ID', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1在库 2借展 3修复 4下架', `cover_url` varchar(512) DEFAULT NULL COMMENT '封面图片', `extra_json` json DEFAULT NULL COMMENT '扩展属性', `deleted` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `idx_cat_status` (`category_id`, `status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='藏品主表';唯一索引 uk_code 保证同一单位的藏品编号不重复,联合索引 idx_cat_status 覆盖了“按分类查状态”的高频查询场景。这种设计下,列表页的过滤条件基本都能命中索引,数据量到几十万条也不慌。
2.2 库房、借展、修复这些关联表怎么建模
藏品主表之外,关联业务表的设计也很有讲究。库房表 storage_room 我用两级模型,一张表存库房和排架位,parent_id 指向父节点,这样在界面上可以做成树形结构。每一件藏品都有一个物理存放位,所以藏品主表里留了 room_id 指向最末级节点。
借展表 exhibition_record 不直接改藏品表里的状态,而是用一条记录来保存“借出时间、归还时间、接收单位、经手人、备注”,同时把藏品主表的 status 字段同步改为 2(借展)或改回 1(在库)。为什么不只改状态不留记录呢?因为馆藏系统的审计要求很严格,每一次状态变化都得查得到原因。这种“业务动作记录表 + 主表状态字段”的配合方式,是馆藏类的标准玩法。
修复表 restore_record 同理,记录修复时间、修复内容、修复单位、费用等。还要注意一个细节:关联表外键我一般不在数据库层面强加约束,而是在 service 层用代码保证引用完整性。原因很简单,馆藏系统未来做数据迁移时,如果外键约束太强会非常痛苦。对于数据的完整性,靠应用层事务去控制,后面 2.4 节会提到。
2.3 常见检索场景和索引优化
馆藏系统真正的技术难点不在增删改,而在“模糊检索”和“组合筛选”。用户经常输入一个藏品名称片段,同时选择年代和分类,期望秒出结果。这里如果直接 SELECT ... WHERE name LIKE '%唐代%',字段没索引时全表扫描会越来越慢。我在索引设计上用了两套方案:
一是名称字段的前缀索引,适合“以关键字开头”的脑补式搜索;二是用 MySQL8.0 内置的 ngram 全文索引,专门解决中文分词问题。全文索引的创建语句类似 FULLTEXT KEY ft_name(name) WITH PARSER ngram,检索时用 MATCH(name) AGAINST('唐代' IN NATURAL LANGUAGE MODE)。实测在几十万条数据的表上,全文索引的响应速度远快于模糊匹配。当然全文索引也有局限,比如对短词的支持一般,所以业务层面还是把 LIKE 和全文索引都保留,根据查询参数自动路由。
MySQL8.0 的窗口函数也是这类系统的一个加分项。比如我想在一个列表里显示“每个朝代收藏数量排名前5的藏品”,用 ROW_NUMBER() OVER(PARTITION BY era ORDER BY create_time) 就能一条 SQL 搞定,不需要写复杂的子查询和临时表。这是 8.0 带给开发者的红利,旧版本 MySQL 做同类统计要麻烦得多。
2.4 数据一致性和事务控制
馆藏数据错一笔可能都要出大问题,所以一致性是这个项目的红线。系统里有几个需要事务保护的场景:录入藏品时,除了插入主表,还要写一条入库流水;借展发起时,要改藏品状态,同时插入借展记录;修复完成时,要更新修复单,可能还要调整藏品状态。这些跨表操作我统一在 service 层方法上加了 @Transactional。
另一个容易踩坑的地方是唯一索引和逻辑删除的冲突。如果藏品编号做了唯一索引,删除时用逻辑删除标记 deleted=1,再次插入相同编号的藏品位会导致唯一索引冲突。我处理时没有简单地把编号拼上 deleted 状态,而是在唯一索引里加入了 deleted_tag 字段:新记录 deleted_tag 取主键ID,删除时把 deleted_tag 改成当前时间戳,这样同名编号不会被唯一索引拦住,业务上也说得通。遇到并发场景,比如两个人同时登记同一编号的藏品,我在 insert 前置阶段做一次编号查询,数据库层面再用唯一索引兜底,基本不会出问题。
3. 后端实现:SpringBoot2 + MyBatis-Plus 落地细节
3.1 MyBatis-Plus 怎么接入和配置
后端代码里,MyBatis-Plus 承担了绝大多数单表和简单联表的 CRUD。接入方式不复杂,pom 引入 mybatis-plus-boot-starter,然后在 application.yml 里配置逻辑删除、驼峰映射和分页插件:
mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0分页插件在配置类里声明一个 MybatisPlusInterceptor Bean,添加 PaginationInnerInterceptor,之后调用 Page 对象就能完成分页查询。我这里把常用的 BaseMapper 方法做个总结,方便对照:
| 场景 | 使用方式 |
|---|---|
| 单条按ID查 | selectById |
| 条件单查 | selectOne(LambdaQueryWrapper) |
| 分页条件查 | selectPage(page, wrapper) |
| 批量新增 | saveBatch(list) |
| 批量按ID删 | removeBatchByIds(ids) |
| 条件更新 | update(null, LambdaUpdateWrapper) |
使用 MyBatis-Plus 最大的感受是,常规业务可以完全不用写 XML SQL,代码量能比传统 MyBatis 少一半。但也要克制,遇到多表关联这种复杂查询,我建议还是老老实实写 Mapper XML 或者 @Select 注解,而不是硬用 QueryWrapper 去拼 join,因为 QueryWrapper 不支持多表关联,强行嵌套反而可读性极差。
3.2 统一响应体和异常处理
和前端联调时最怕各接口返回格式不统一。这个项目从一开始就固定了 Result 结构:code、message、data 三个字段。成功时 code=200,业务异常时 code 变成自定义错误码,系统异常 code=500。所有 controller 接口直接返回 Result 对象,由全局异常处理器统一兜底。
异常处理这块我用 @RestControllerAdvice 加 @ExceptionHandler。业务异常 BizException 绑定了枚举错误码,比如藏品编号重复、文件类型不允许、参数校验失败等;意料之外的异常统一打印日志并返回“系统繁忙”。这一套做完,前端 axios 拦截器只需要看 Result.code 就能统一提示,代码非常清爽。特别是文件上传场景,如果后端直接抛了异常,前端 upload 组件的 on-success 是等不到回调的,只有统一异常处理才能把错误转换成可读的 JSON 响应。
3.3 权限模型:RBAC + 行级数据权限
馆藏系统不是面向公众的开放平台,而是内部办公系统,所以权限控制必须精细到角色和数据范围两个维度。我在用户表、角色表、菜单关系表的基础上,实现了经典的 RBAC 模型:超级管理员拥有全部权限,业务管理员可以操作藏品和审核,普通录入员只能新增和编辑自己负责的库房数据,访客只能查看。
实现上我用 JWT 做登录态。登录成功后服务端生成 token,前端每次请求在 header 里携带,后端通过拦截器解析 token 并把当前用户信息放入 ThreadLocal 上下文。对于需要权限校验的接口,用自定义注解标注,然后在 AOP 里判断角色,代码整洁也不侵入业务。
行级数据权限这块,我遇到的实际需求是“录入员只能看到自己库房的藏品”。如果只在查询时手动加条件,每条 SQL 都得记得补,很容易漏。我在拦截器层面做了数据权限的自动拼接:解析当前用户绑定的库房范围,如果角色没有全部库房权限,就自动在 SQL 上追加 AND room_id IN (...)。这个能力基于 MyBatis-Plus 的 InnerInterceptor 实现,属于比较进阶的用法,但对这类多租户式场景价值很大。
3.4 文件上传与图片访问路径
藏品封面和附件是这个系统的刚需。上传我用 MultipartFile + 本地磁盘存储 + Nginx 静态映射的方式,不引入额外的对象存储中间件,降低部署复杂度。上传文件的逻辑:校验文件大小和扩展名,按日期创建目录,生成随机文件名,最后把访问 URL 写入 sys_file 表。限制文件类型时不要光看扩展名,还要读一下文件的 Content-Type,防止被改扩展名绕过。
这里有一个新手容易犯的错:保存文件时只存了磁盘路径,页面显示图片直接引用本地绝对路径,线上部署后根本访问不到。正确做法是存相对路径或完整 URL,由 Nginx 或网关做静态资源映射,开发环境和生产环境只需改配置前缀。图片访问路径如果涉及跨域,也需要在 Nginx 层统一加跨域头,否则前端经常出现图片能打开但 canvas 或上传组件获取不到内容的情况。
4. Vue3 前端:Vite + Element Plus 搭建与联调经验
4.1 Vue3 工程化搭建与技术取舍
前端我直接选了 Vite 作为构建工具,Vite 的冷启动和热更新体验比 Webpack 好很多,Vue3 项目基本已经是标配。创建项目用的 npm create vite@latest,然后手动安装 vue-router、pinia、axios 和 element-plus。有人会问要不要用 TypeScript,这个项目考虑到团队习惯没有上 TS,但结构上保留了类型友好的写法。对于上 TS 的项目,我也顺手整理过若依 Vue3 TS 模板的报错处理方式,后面 5.4 节提到。
Composition API 和 Options API 的区别,在这个项目里体现得很明显。简单页面用 Options API 也够用,但当多个页面都需要“加载状态、分页参数、查询条件”时,Composition API 可以用自定义 hook 把这套逻辑抽出来。我写了一个 useCollectionList 的 hook,负责调用接口、维护 loading、刷新列表,页面组件里只需要调用这个 hook 再绑定模板,复用效率大幅提升。对于 Vue3 新手,我建议直接学 Composition API,不用再惦记 Options API 的老写法。
前端目录结构按业务域拆,router 用 createRouter 配置静态路由,store 用 Pinia 管理当前用户信息和权限标记。这里有个取舍:如果菜单权限由后端动态下发,路由需要改成动态注册模式;如果只是固定菜单,静态路由就够。我做了固定菜单 + 按钮级权限控制的方案,权限标记用自定义指令 v-permission 控制按钮显隐,简单有效。
4.2 核心页面:藏品列表、搜索与分页
藏品列表页是前后端联调的练兵场。实现思路:搜索表单上放年代、分类、关键字几个筛选条件,点击查询触发列表接口,接口返回的数据绑定在 el-table 上,底部用 el-pagination 处理分页。Vue3 下用 ref 定义响应式数据,onMounted 里调用初始化方法。
<script setup> const searchForm = ref({ keyword: '', era: '', categoryId: null }) const tableData = ref([]) const total = ref(0) const loading = ref(false) async function fetchList(pageNum = 1, pageSize = 10) { loading.value = true const res = await listCollections({ ...searchForm.value, pageNum, pageSize }) tableData.value = res.rows total.value = res.total loading.value = false } </script>搜索按钮和重置按钮的逻辑也和大部分后台一样,但有一个小优化:搜索表单我在输入关键字处加了 debounce 防抖,当用户输入完停住 500ms 后自动触发查询,省得频繁点搜索按钮,体验顺滑很多。表格里的藏品封面用 el-image 懒加载,缩略图统一加 style 限定尺寸,避免原图过大拖慢页面。
4.3 axios 封装、响应拦截与文件下载
所有接口请求都走一个封装好的 http 实例。axios.create 设置 baseURL 和 timeout,请求拦截器里把 token 从 Pinia 或 localStorage 取出来加到 header;响应拦截器里先剥掉包装层,如果 code 不是 200 则统一用 ElMessage 报错,并针对 401 做跳转登录的处理。
下载文件这块也有讲究。如果是后端返回文件流,axios 默认无法把 blob 直接给到浏览器下载,要在响应拦截器里判断 responseType 是不是 blob,然后单独走下载逻辑:
const blob = new Blob([res.data], { type: 'application/vnd.ms-excel' }) const link = document.createElement('a') link.href = URL.createObjectURL(blob) link.download = filename link.click()导出藏品台账就是走的这条链路,用户点击导出后稳定拿到 Excel 文件。要特别注意的是,后端在导出接口的响应头里要正确设置 Content-Disposition,文件名做 URL 编码,否则中文乱码会非常难看。
4.4 表格、表单和上传组件的配合
馆藏系统的信息录入页面比普通表单复杂,因为藏品字段多,还要带图片上传。Element Plus 的 el-form 配合 rules 做校验,日期选择器用 el-date-picker,年代建议用下拉框而不是自由输入,免得有人填“唐”有人填“唐代”。表单提交时先 validate,成功后调新增接口,再返回列表页刷新。
上传图片我用了 el-upload 组件,设置 action 指向后端上传接口,name 设为和后端接收参数一致,on-success 里判断响应码。我踩过的坑是:一旦后端返回结构不是前端预期的结构,on-success 永远不触发,反而会触发 on-error。比如后端异常处理返回了 code=500 的 JSON,上传组件就认为成功了,页面却拿不到图片 URL。解决方法是把 on-success 的响应体强校验,同时利用 on-error 兜底错误提示。这一块的坑对于所有使用 Element Plus 上传组件的项目都有参考价值。
5. 常见问题与排查技巧实录
5.1 前后端跨域问题
联调阶段最频繁遇到的就是跨域,我用了一段典型的报错:“Access to XMLHttpRequest at ... has been blocked by CORS policy”。解决方案很简单:后端配置一个全局 CorsFilter,允许指定 origin、方法、请求头。注意不要用 @CrossOrigin 加在 Controller 上,因为一个接口漏加就又要排查半天。还有一个细节是,当接口报错时响应头可能不带 CORS 信息,浏览器显示的跨域报错其实是业务异常被跨域拦截的假象,排查时要先看接口本身是否通。
5.2 MySQL8.0 连接串和时区问题
MySQL8.0 的驱动类名是 com.mysql.cj.jdbc.Driver,连接串要带 serverTimezone。这个问题我见过不少人栽跟头,不配时区启动时直接报连接错误。正常配置:
jdbc:mysql://localhost:3306/museum?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=trueallowPublicKeyRetrieval=true 是针对 MySQL8 的认证方式,本地开发经常需要加上,否则连接时可能报 Public Key Retrieval is not allowed。生产环境建议去掉 SSL 相关参数,同时连接池把 druid 或 hikari 的 maximum-pool-size 调到一个合理的值,避免高峰期连接数不够。
5.3 Lombok 编译报错
开发过程中有一个经典的报错:“You aren't using a compiler supported by lombok, so lombok will not work with your project.” 这个问题一般发生在 JDK 版本升级之后,或者 IDEA 的 Lombok 插件版本太旧。我处理过两次,一次是升级 lombok 依赖版本到 1.18.30 以上解决,另一次是 IDEA 里勾选了 “Enable annotation processing”。这种问题在协作开发里特别影响士气,因为只有一个同事编译不过,其他人好端端的,排查方向先锁定 IDE 注解处理和 Lombok 版本。
5.4 若依 Vue3 TS 报错的参考处理
因为看过不少人在用若依框架的 Vue3 TS 版本时遇到类型报错,这里顺手整理一下通用处理方法。第一种是 vue-tsc 严格模式报错,解决方案是把 tsconfig 的 strict 改为 false;第二种是导入组件时找不到类型声明,需要在 env.d.ts 里补充声明文件;第三种是第三方库本身类型不完整,用 // @ts-ignore 或声明模块绕过去。虽然本项目没上 TS,但 vue3 后台管理的类型问题大同小异,遇到不要慌,先把报错信息读明白再动手。
5.5 MyBatis-Plus 批量插入性能优化
馆藏系统初始化导入历史数据时,动辄几千上万条藏品记录入库,一条一条 insert 非常慢。MyBatis-Plus 的 saveBatch 默认批量大小是 1000,但如果连接串没有开启 rewriteBatchedStatements=true,MySQL 并不会真正合并 SQL 去执行,性能提升有限。我在连接串里加上 rewriteBatchedStatements=true 之后,批量导入速度提升了一个量级。这也是 MyBatis-Plus 批量操作中最值得记住的一个参数。
6. 项目部署与文档沉淀
6.1 前后端打包与 Nginx 部署
部署环节我走的是经典的前后端分离方案:后端打成 jar 包跑在服务器,前端 Vite 构建出 dist 静态目录,交给 Nginx 托管。后端打包用 Maven 的 mvn clean package,生成的可执行 jar 用 nohup java -jar 启动,配置好 JVM 参数和日志输出文件。前端 npm run build 之后,dist 文件夹里就是压缩好的静态资源。
Nginx 配置核心是两部分:托管前端静态页面,以及把 /api 前缀的请求反向代理到后端服务端口:
server { listen 80; server_name museum.example.com; location / { root /var/www/museum; index index.html; 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; } }try_files 那一行要特别留意,VueRouter 使用 history 模式时必须配,否则刷新页面会 404。这个坑我见得太多了,很多前端部署到测试环境后反馈“页面一刷新就没了”,基本都在这。
6.2 数据库备份与初始化脚本
数据库脚本我分成了 schema.sql 和 data.sql。schema.sql 负责建库建表,data.sql 负责初始化基础数据,比如管理员账号、字典数据、默认菜单。首次部署时顺序执行即可。线上运行后,我用系统的 mysqldump 做每日备份,备份文件保留最近 7 天,再配合一个简单的 crontab 定时任务把备份文件归档。
MySQL8.0 导入数据时要注意:如果 SQL 文件里包含 utf8mb4 的字符编码,客户端执行导入前最好执行 SET NAMES utf8mb4,否则中文可能会乱码。数据库连接工具我也统一建议用 MySQL8 兼容的版本,老版本的 Navicat 连 MySQL8 需要走 caching_sha2_password 验证方式,有时会连不上。
6.3 文档结构怎么组织才对交接有用
标题里写着“含文档”,这部分我必须多说两句。很多项目的文档形同虚设,要么只有 README 写启动步骤,要么线上文档和代码严重脱节。我做这个项目时把文档拆成三份:一份是部署手册,写清楚环境要求、初始化步骤、启动命令;一份是接口说明,按模块列出关键接口的入参出参;还有一份是设计文档,记录表结构、权限模型和业务流转逻辑。这样无论接手的人是运维、前端还是新后端,都能各取所需。
文档最大的价值是降低交接成本,所以写的时候要克制:不要大段贴源码,而是写“为什么这么设计”和“改哪里会影响什么”。比如修改藏品状态的核心逻辑在 CollectionService 的 changeStatus 方法里,文档里指出这一点,后来的人改需求时就不会误伤其他模块。
6.4 上线之后还能扩展什么方向
这套馆藏系统上线后是能继续生长的。如果后续要做公开门户,可以把藏品列表做成只读接口给门户网站调用;如果要增强检索体验,可以接入 ElasticSearch 做全文搜索;如果要支持远程盘点,可以再加移动端适配层。扩展方向很多,但底层的数据模型如果一开始没搭好,后面每一步都要返工。这也是我为什么在这个项目里花大量篇幅在数据建模和权限设计上,而不是堆功能。
我个人在实际操作中的体会是,馆藏系统这种项目,真正的复杂度从来不在代码本身,而在业务规则的梳理和数据口径的统一。你先把“一件藏品从进馆到出馆会经历哪些状态、每个状态由谁触发、需要记录什么信息”这个问题想清楚,后面的编码工作基本就是体力活。另外,如果这个系统的代码框架你已经吃透了,下一步想进阶的话,可以从数据迁移工具、审计日志可视化、批量导入模板这几个方向继续玩,这些都是博物馆数字化里非常实际的痛点。
最后再分享一个小技巧:这类系统上线前,一定找业务人员做一次全流程冒烟测试,从录入藏品到发起借展再到归还,走一遍真实业务。前端和后端各自身逻辑都通,但拼起来业务跑不通的情况我遇到过太多次了。这套 SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0 的组合本身已经很成熟,只要业务模型扎实,项目就成功了一大半。