做文博信息化的项目,最容易被问的一句话是:你们这个系统,到底解决了管理上什么问题?我在拆解红色革命文物征集管理系统的时候,把这个问题拎出来想了很久。传统征集工作主要靠线下登记、纸质流转,一件文物从线索提交到最终入库,中间要过征集员、鉴定专家、分管领导多个角色,流程一长,信息就容易断在某个节点。这套系统要做的,就是把征集全流程搬到线上,让每一步都有记录、可追溯、能统计。
从技术实现上看,这是典型的 Java Web MVC 项目:后端 SpringBoot2 + MyBatis-Plus + MySQL8.0,前端 Vue3。它覆盖了登录鉴权、征集公告、线索登记、鉴定评估、入库归档、数据统计这些完整环节。适合正在做前后端分离课题、想系统理解 SpringBoot 与 Vue3 配合方式、或者对文博行业信息化感兴趣的同学参考。下面我会按业务拆解、数据库设计、后端实现、前端落地、部署排雷、答辩复盘这条线,把整个系统的关键点全部过一遍。
1. 项目拆解:一套文物征集系统背后的技术取舍
1.1 征集管理系统的核心业务是什么
先把业务说清楚,再谈技术。一场红色主题文物征集活动,从发公告开始就进入流程:社会公众看到公告,提交线索或捐赠意向;征集员对线索做初审,判断是否符合征集范围;符合的进入鉴定评估环节,由鉴定专家对实物或照片出具意见;领导根据鉴定意见决定是否征集;最终移交库房,完成入库登记,形成文物档案。
这串流程的关键在于“多角色、多节点、多单据”。线下做的时候,线索登记表在征集员手里,鉴定意见表在专家手里,审批单在领导手里,各环节之间的衔接完全靠人工传递。系统上线后,每个环节都变成一个状态节点,每一笔操作都有操作人、操作时间、操作记录。我常拿寄快递来类比:每一站都要扫码,扫到哪算哪,谁扫的都得留痕。
所以这套系统的核心不是页面做得有多花哨,而是状态流转不能乱、角色权限不能串、每一步操作都可追溯。业务上的这个特点,直接决定了后面的数据表设计和技术选型。
1.2 为什么是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0
这套技术组合在中小型管理系统里几乎是一套标准答案,原因很实在。
SpringBoot2 的生态足够成熟,资料多,遇到问题很容易查。最关键的是一套 Java 后端服务从零搭建到跑通,配置文件极少,内置 Tomcat,打包成 jar 就能部署。Vue3 的组合式 API 对复杂表单页面特别友好,征集登记页动辄几十个字段,按业务分组写在 setup 里,比 Vue2 的 Options API 把 data、methods、computed 拆得七零八落要好维护得多。
MyBatis-Plus 解决的是样板代码问题。单表 CRUD 不用写 XML,BaseMapper 直接提供 insert、update、delete、selectPage,条件构造器做动态查询非常顺手。MySQL8.0 默认字符集就是 utf8mb4,能直接存生僻字,窗口函数做统计对比也方便,后面做征集数据按月份对比只需要一条 SQL。
另外这个项目是含文档的,配套了设计文档、数据库脚本、部署说明,这在实际开发里帮了大忙。拿到一个带文档的项目和拿到一个只有代码的项目,差别在于你能不能在一天之内搞清楚业务全貌。文档里把 ER 图、接口清单、状态流转图都画好了,上手成本会低一大截。
1.3 MVC分层模型如何落到文物征集场景
很多同学对 MVC 的理解停留在“Model-View-Controller”三个字母上,落到实际项目里反而不知道每一层该写什么。这个项目的分层是这样的。
前端 Vue3 本身是 MVVM,它通过 Axios 调后端接口。后端严格按 Controller-Service-Mapper 三层划分:Controller 只做“收转发”,接收参数、调用服务、返回统一结果,里面尽量不写业务逻辑;Service 层写业务规则,比如判断当前状态能不能流转、校验必填项、组装统计数据;Mapper 层对应 MyBatis-Plus 的 BaseMapper,复杂 SQL 时再写 XML 或注解。
实体层与数据库表一一对应,接口入参用 DTO,接口返回用 VO,避免把内部字段直接暴露给页面。这样分完以后有个很明显的好处:改需求的时候,你大致能猜到应该改哪层。比如前端要新增一个字段展示,先改 VO 和 DTO;要加一种状态更新逻辑,改 Service;要优化查询速度,再去动 Mapper 或 SQL。
2. 数据库设计:把文物“身份”和征集“流程”分开建模
2.1 主表与流水表怎么拆,字段怎么定
我设计表结构时定了一条原则:文物档案是一等公民,征集流程是围绕档案展开的流水记录,两者必须分表。文物档案表 revolution_artifact 记录这件文物的静态属性,包括文物编号、名称、年代、类别、质地、尺寸、完残程度、来源、流传经历、征集方式、征集状态;征集线索表 collection_line 记录每一次征集动作,从线索提交到最终入库的过程全部留在流水表里。
为什么这么拆?因为现实中同一件文物可能被多次征集、可能先被驳回又被重新提交、可能从征集线索变成征集计划又变成入库档案。如果把征集流程直接挂在文物表上,表结构会非常别扭。反过来,把线索表独立出来之后,任何一次征集的来龙去脉都是可查的。
关键字段设计上,我的经验是名字越直白越好。比如 artifact_name、artifact_code、period、material、source_type、donor_name、contact_phone、budget_amount、receive_time。注意项目里所有日期时间统一用 datetime 类型,金额用 decimal(10,2),描述类字段用 text,避免后期因为字段类型定得太小返工。
2.2 征集状态机:七个状态流转的幂等设计
状态流转是这套系统最核心的业务逻辑。我把征集状态设计成整型状态码,便于排序和统计。状态码定义如下:
| 状态码 | 状态名称 | 说明 |
|---|---|---|
| 0 | 待初审 | 线索刚提交,等待征集员处理 |
| 1 | 初审通过 | 征集员确认线索有效 |
| -1 | 初审驳回 | 线索不符合征集范围 |
| 2 | 待鉴定 | 初审通过后进入专家鉴定环节 |
| 3 | 鉴定通过 | 专家确认文物基本价值 |
| -2 | 鉴定不通过 | 专家否定征集价值 |
| 4 | 待审批 | 鉴定通过后等待领导审批 |
| 5 | 审批通过 | 同意征集,准备进入入库 |
| -3 | 审批退回 | 审批未通过,退回重新处理 |
| 6 | 已入库 | 完成入库登记,关联文物档案 |
为什么要用整型而不是字符串?一是排序方便,二是统计接口里按状态做 group by 直接用数字分组,三是在状态流转记录表里存数字占用空间小。可读性低的问题用一个枚举类解决,Java 代码里定义状态常量,前端用字典表映射中文名称。
状态流转时最怕重复提交。征集员手一抖,同一个按钮双击,很可能就出现两条同样的流转记录。我在 Service 层做流转时全部采用带前置状态的更新语句,这也是幂等设计的关键:
boolean updated = collectionLineMapper.update( null, new LambdaUpdateWrapper<CollectionLine>() .set(CollectionLine::getStatus, 1) .eq(CollectionLine::getId, lineId) .eq(CollectionLine::getStatus, 0) ); if (!updated) { throw new BusinessException("当前状态已变更,请勿重复操作"); }这条 SQL 的意思是:只有在线索当前状态还是 0 的时候,才允许改为 1。如果别人已经改过,影响行数就是 0,直接提示“请勿重复操作”。数据库底层帮我们保证了状态不会重复流转,比在代码里先查后改再更新要可靠得多。
每次状态变更都会写入一张 collection_line_log 表,记录 from_status、to_status、operator_id、operator_name、remark、operate_time。这样任何一个流程节点出了问题,都能倒查是谁在什么时候做了什么操作。
2.3 附件、逻辑删除与审计字段的几个细节
文物征集必然涉及图片和文档,比如文物照片、捐赠意向书、鉴定意见书。我单独设计了 file_record 表,字段包括 business_type、business_id、origin_name、store_path、file_size、upload_by、upload_time。
为什么不用 blob 把文件直接塞进数据库?因为文件数据和业务数据的生命周期不一样。业务数据要参与查询和统计,文件只需要能存取就行。把二进制塞进业务表会让每张表都变重,备份和迁移时特别痛苦。正确做法是文件落磁盘或对象存储,数据库里只存路径索引。
逻辑删除字段我统一用 deleted,配合 MyBatis-Plus 的 @TableLogic 注解,删除操作会自动变成 UPDATE,列表查询会自动加 deleted=0 条件。做文博类系统不建议物理删除任何记录,原因很简单:征集工作讲究可追溯,一条误删的记录可能意味着责任无法厘清。
审计字段是 create_time、update_time、create_by、update_by,由 MetaObjectHandler 自动填充。谁建的数据、谁最后改的、什么时候改的,查出来一目了然。这也是评审时经常被问到的点,后面会细说。
3. 后端核心实现:MyBatis-Plus让CRUD边界更清晰
3.1 分页与条件构造器:列表查询的实战写法
列表查询是所有管理系统的标配,难在条件组合多。征集线索列表要支持按状态筛选、按关键字搜索、按提交时间范围查询、按征集方式过滤,如果每个条件都手写 if 拼 SQL,代码会很难看。
MyBatis-Plus 的条件构造器正好解决这个问题。先配置分页插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }然后业务代码里这样写:
Page<CollectionLine> page = new Page<>(pageNo, pageSize); LambdaQueryWrapper<CollectionLine> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(status), CollectionLine::getStatus, status) .like(StringUtils.hasText(keyword), CollectionLine::getArtifactName, keyword) .ge(startTime != null, CollectionLine::getCreateTime, startTime) .le(endTime != null, CollectionLine::getCreateTime, endTime) .orderByDesc(CollectionLine::getCreateTime); Page<CollectionLine> result = collectionLineMapper.selectPage(page, wrapper);这里第一个参数是“条件成立才拼这个条件”。前端传了 status 才拼 status 筛选,没传就跳过。LambdaQueryWrapper 最大的好处是字段名用方法引用,不直接写字符串列名,将来重构字段名的时候编译器能直接报错,不会因为手误把 SQL 关键字写错而排查半天。返回给前端时,实体转 VO,隐藏掉内部审计字段。
3.2 字段自动填充与逻辑删除:少写两百行样板代码
每个表的 create_time、update_time 如果都在业务代码里手动 set,会非常啰嗦。MyBatis-Plus 的字段自动填充可以一次性解决。
先实现 MetaObjectHandler:
@Component public class MyMetaObjectHandler implements MetaObjectHandler { @Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, "createBy", Long.class, getCurrentUserId()); this.strictInsertFill(metaObject, "updateBy", Long.class, getCurrentUserId()); } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now()); this.strictUpdateFill(metaObject, "updateBy", Long.class, getCurrentUserId()); } }实体字段上标注填充时机:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; @TableLogic private Integer deleted;这样写业务代码的时候,完全不用关心谁来维护时间戳和操作人,插入数据自动补全,更新数据自动刷新。逻辑删除字段也是一个注解的事,调用 mapper.deleteById(id) 时,MyBatis-Plus 会自动把语句改写成 UPDATE 设置 deleted=1;查询时自动追加 deleted=0 条件。做文物类系统一定要用逻辑删除,这不是技术洁癖,是业务要求数据必须能找回。
3.3 Excel导入导出:征集清册一键生成
征集工作中经常要导出一份线索清册给专家评审组,或者批量导入一批线下登记的线索。用 EasyExcel 处理非常顺。
导出线索清册的核心代码如下:
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); String fileName = URLEncoder.encode("征集线索清册", "UTF-8"); response.setHeader("Content-Disposition", "attachment;filename=" + fileName + ".xlsx"); EasyExcel.write(response.getOutputStream(), CollectionLineExcelVO.class) .sheet("征集线索") .doWrite(list);VO 上用 @ExcelProperty 注解控制列名和顺序:
public class CollectionLineExcelVO { @ExcelProperty("文物名称") private String artifactName; @ExcelProperty("年代") private String period; @ExcelProperty("来源") private String source; @ExcelProperty("联系人") private String contactName; @ExcelProperty("联系电话") private String contactPhone; @ExcelProperty("备注") private String remark; }导入我一般用 ReadListener 逐行处理,每读一行就校验并入库,遇到脏数据记录到错误列表,最后统一返回错误提示给前端。
这里有个我踩过的坑:导出的模板一定要从代码里生成给业务人员用,不要让人工在 Excel 里手动改表头。人工改过的模板经常多出不可见空格或格式不一致,程序读出来字段对不上。代码生成模板,至少保证列名和类型是可控的。
另一个坑是导出文件名的中文乱码。浏览器对 Content-Disposition 的编码处理不一致,中文文件名必须用 URLEncoder 编码,否则下载下来的文件名是一串乱码。
3.4 JWT鉴权:征集员、鉴定专家、管理员各管一摊
系统的角色不多,就是系统管理员、征集员、鉴定专家、领导四类,所以我没引入 Spring Security,而是用 JWT + 拦截器 + 自定义注解做了一个轻量鉴权方案。
登录接口校验用户名密码,登录成功生成一个 JWT,把 userId、角色标识、过期时间放进去,前端后续请求在 Header 里带 Authorization: Bearer token。
自定义一个 @RequireRole 注解,标注在需要角色控制的接口上:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface RequireRole { String[] value(); }拦截器里解析 token,取出角色,和注解要求的角色做交集判断。这样做的好处是从 Controller 代码层面就能看到接口的权限要求,比如鉴定专家提交意见的接口标注 @RequireRole({"EXPERT"}),管理员审核用户的接口标注 @RequireRole({"ADMIN"}),征集员登记线索的接口标注 @RequireRole({"COLLECTOR"})。
不用 Spring Security 的原因很简单:这个项目的角色模型只有三四种,权限控制到接口级别就够用了。引 Security 反而要配置一堆过滤器链和用户服务,学习成本高,调试也麻烦。如果你的项目角色多、权限细到按钮级,再考虑升级到 Spring Security 或 Sa-Token。
4. 前端Vue3落地:组合式API改造后台系统的关键点
4.1 初始化工程:Vite、Pinia、Element Plus 怎么配
前端我用 Vite 创建项目,Vue3 搭配 TypeScript 是现在的标配做法。初始化命令很简单:
npm create vue@latest选择 TypeScript、Router、Pinia、ESLint 这几项,ESLint 建议选严格模式,团队协作时能统一代码风格。目录结构大致如下:
src/ api/ 接口请求层 assets/ 静态资源 components/ 通用组件 router/ 路由配置 stores/ Pinia 状态管理 views/ 页面组件 utils/ 工具函数状态管理用 Pinia 而不是 Vuex,官方的解释是 Pinia 更轻量、对 TypeScript 支持更好、去掉了 mutation 概念,store 本身就是组合式 API 的风格。对本项目来说,登录用户信息、token、角色菜单都适合放在 Pinia 里,页面组件之间共享状态很方便。
UI 组件库用 Element Plus,图标库用 @element-plus/icons-vue。在 main.ts 里全部注册,开发效率最高。需要注意 Vue3 生态的组件库基本都是按需引入,全量引入的好处是省心,代价是打包体积大一点,管理后台项目优先考虑省心。
4.2 Axios封装:登录态与统一错误处理
前后端对接一定要统一封装 Axios,否则每个页面都要重复处理 token 和错误提示。
请求拦截器里做两件事:把 token 塞进 Header,统一处理 loading 状态。
const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.request.use(config => { const authStore = useAuthStore() if (authStore.token) { config.headers.Authorization = `Bearer ${authStore.token}` } return config })响应拦截器统一处理业务码:
service.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } if (res.code === 401) { const authStore = useAuthStore() authStore.logout() router.push('/login') return Promise.reject(new Error('登录已过期')) } ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) }, error => { ElMessage.error(error.message || '网络异常') return Promise.reject(error) } )这样统一处理之后,页面里调接口只需要关注业务数据本身,不用再写一堆 try-catch 和错误弹窗。接口文件里按模块拆分,比如 collection.ts 里放线索相关的所有请求,file.ts 里放上传相关请求,结构清晰,改动时能快速定位。
4.3 三步征集表单:组件拆分和联动逻辑
征集线索登记是系统里最复杂的表单,涉及文物基本信息、来源信息、鉴定材料三块内容。我把它做成一个三步表单,顶部用 ElSteps 显示进度,每个步骤对应一个子组件,父组件用 reactive 维护一个大的 form 对象。
核心思路是子组件只提交字段变更,不维护整套表单数据:
const form = reactive({ artifactName: '', period: '', material: '', sourceType: 'donate', donorName: '', contactPhone: '', budgetAmount: null, images: [] })不同征集方式对应不同字段。选择“捐赠”时显示捐赠人姓名和联系方式,选择“收购”时显示预算金额,选择“调拨”时显示来源单位。这个联动我直接用 watch 监听 sourceType:
watch(() => form.sourceType, val => { if (val === 'donate') { form.budgetAmount = null } if (val === 'purchase') { form.donorName = '' } })附件上传用 ElUpload:
<el-upload :action="'/api/file/upload'" :headers="{ Authorization: 'Bearer ' + authStore.token }" @success="handleUploadSuccess" > <el-button type="primary">上传文物照片</el-button> </el-upload>一个很容易忽略的细节:ElUpload 的 action 是直接发请求的,不会经过你封装的 Axios 实例,所以 Authorization 头必须手动传,否则后端鉴权拦截器会直接拦下来。
4.4 ECharts统计面板:征集来源与年代分布
首页统计面板我分了两个维度:征集方式分布用饼图,征集年代分布用柱状图。后端统计接口返回的数据结构很简单:
{ "donate": 120, "purchase": 18, "transfer": 6, "exchange": 3 }前端拿到数据后给 ECharts 的 option 赋值:
const chart = echarts.init(document.getElementById('sourceChart')) chart.setOption({ tooltip: { trigger: 'item' }, series: [{ type: 'pie', radius: '60%', data: [ { name: '捐赠', value: data.donate }, { name: '收购', value: data.purchase }, { name: '调拨', value: data.transfer }, { name: '交换', value: data.exchange } ] }] })做图表最容易栽的坑是容器高度为 0,导致图表完全空白。div 必须设置明确高度,比如 style="height: 400px",光设置宽度没用。另一个是老生常谈的问题:浏览器窗口尺寸变化时要调用 chart.resize(),否则图表在窗口放大后会糊掉。在 onMounted 里注册 window resize 监听,beforeUnmount 里移除。
5. MySQL8.0部署与联调排雷
5.1 安装MySQL8.0后必须改的两个配置
MySQL8.0 安装本身不算难,难的是装完以后的基础配置。在 my.cnf 里我固定会配置这几项:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci default-time-zone=+08:00 default_authentication_plugin=mysql_native_password字符集必须设成 utf8mb4,否则遇到生僻字或特殊符号会直接报错或存成乱码。时区必须设成 +08:00,否则 SpringBoot 通过 JDBC 连接时,如果 url 里又没带 serverTimezone 参数,会直接报 unrecognized time zone 错误。
8.0 默认的认证插件是 caching_sha2_password,老版本的驱动不一定兼容。如果你项目里用的 mysql-connector-java 版本偏旧,连接时会报 Public Key Retrieval is not allowed,这时有两个解决办法:要么把驱动升级到 8.0.x,要么在 my.cnf 里把 default_authentication_plugin 改回 mysql_native_password。我更推荐升级驱动,因为新协议本身更安全,没必要为了兼容老驱动降低安全等级。
建库时也一定要显式指定字符集:
CREATE DATABASE `red_collection` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;5.2 SpringBoot + MySQL8.0的经典报错与处理
把我在部署过程中实际遇到的报错整理成一张表,基本覆盖了最常见的几个坑:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
| No suitable driver found | 驱动未引入或版本太老 | 使用 mysql-connector-java 8.0.x |
| Public Key Retrieval is not allowed | 认证协议不匹配 | url 加 allowPublicKeyRetrieval=true |
| The server time zone value is unrecognized | JDBC 连接未指定时区 | url 加 serverTimezone=Asia/Shanghai |
| Unknown database | 数据库不存在或库名不对 | 执行建库 SQL,检查库名 |
| Access denied for user | 密码错误或账号权限不足 | 检查 MySQL 用户授权 |
SpringBoot 配置文件里连接字符串我固定写成:
spring: datasource: url: jdbc:mysql://localhost:3306/red_collection?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver特别注意 driver-class-name 在 MySQL8.0 下必须是 com.mysql.cj.jdbc.Driver,老驱动类名 com.mysql.jdbc.Driver 在 8.0 驱动里已经废弃了。
5.3 前后端联调:代理、跨域与上传大小
开发环境下前后端端口不同,必然会有跨域问题。我的做法是前端配 Vite 代理,后端不额外开 CORS,这样既减少暴露面,又避免重复配置。
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }生产环境用 Nginx 反向代理:
location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意 proxy_pass 后面有没有斜杠是有讲究的:不带斜杠时,会把完整路径原样转发给后端;带斜杠时,匹配的 /api/ 前缀会被替换掉。如果后端接口本身不带 /api 前缀,这里要处理清楚。
还有一个高频问题:文件上传报 MultipartException。SpringBoot 默认单个文件最大 1MB,征集照片动辄几 MB,必须调大:
spring: servlet: multipart: max-file-size: 20MB max-request-size: 20MB调完之后建议重启后先用 Postman 测一个大文件上传,直接确认后端配置生效,再回前端联调,能省不少排查时间。
6. 答辩、部署与项目复盘中的实用经验
6.1 评审时容易被追问的几个问题怎么回答
做这类完整项目,最怕的往往不是开发,而是答辩或评审时被问到设计决策的问题。我整理了四个必问点,附上回答思路。
为什么不用 MongoDB 或 Redis 存业务数据?业务模型是一对多、多对多强关系结构,征集线索、鉴定记录、文物档案之间存在明确的关联查询和事务需求,关系型数据库是合理选择。Redis 可以做缓存和验证码存储,但放到核心业务数据上并不合适。
MyBatis-Plus 会不会影响性能?单表 CRUD 不需要手写 SQL,多表查询和复杂统计仍然可以写原生 SQL。项目数据量在十万级以内,加好索引后性能完全没有压力,开发效率提升才是当前更看重的。
征集状态出错了怎么办?每个状态流转都做了幂等控制,并且写入了日志表。系统提供回退入口,负责人可以把错误流转的记录回退到上一状态,回退操作本身也会记录日志。
文件都存本地服务器,不会有问题吗?对单机部署的小型系统够用。扩展方案是把文件迁移到 MinIO 或 OSS,数据库里只需要存访问 URL,文件表结构不需要改。
6.2 我踩过的三个坑与速查表
第一个坑是 MySQL8.0 的时区问题。当时 url 里没写 serverTimezone,启动项目后第一次连数据库直接抛 unrecognized time zone,排查了一个多小时才发现是连接串缺参数。这个问题现在几乎成了 MySQL8 时代的新手必经之路。
第二个坑是 Element Plus 表单校验不触发。在 Vue2 里 el-form 的校验规则写在 data 里就能生效,Vue3 + Element Plus 版本里规则要放在响应式结构里,触发条件要和组件事件对得上,否则点提交按钮校验静默失败。
第三个坑是 MyBatis-Plus 的 saveBatch 不是真正意义上的批量插入。它内部是循环执行单条 insert,只是帮你省了代码。数据量大的时候性能并不理想,真需要批量插入,要在 JDBC url 里加 rewriteBatchedStatements=true,并且使用 ExecutorType.BATCH。
常见问题速查表如下:
| 问题 | 排查方向 | 建议 |
|---|---|---|
| 列表接口慢 | 看 SQL 是否走了索引 | 状态、时间字段加联合索引 |
| 图片上传失败 | 检查 Spring 上传大小限制 | 适当调大 max-file-size |
| 前后端联调返回 401 | 检查 token 是否过期或缺失 | 排查请求头 Authorization |
| 部署后页面白屏 | 检查 dist 路径和 Nginx 配置 | 调整 try_files 指向 index.html |
| 字符集乱码 | 检查数据库、连接串、前端 charset | 统一使用 utf8mb4 |
6.3 系统还能往哪些方向扩展
一套成熟的征集管理系统,做完核心流程之后还有不少扩展空间。如果后续想升级,我认为有五件事最值得做。
给征集线索登记增加 OCR 识别能力,拍摄文物铭文或藏品编号后自动识别填入表单,减少人工录入。捐赠意向书在线签署,引入电子签章服务,替代线下纸质签字。移动端离线采集,做一个小程序或 App,支持走访征集线索时在无网环境下登记,回到有网环境后同步。统计大屏,把征集数据按地区、年代、来源方式做成可视化的 LED 大屏展示,适合文博单位对外展示工作成果。与上级藏品管理系统对接,文物入库后自动生成藏品编号并同步上报,打通数据孤岛。
我个人复盘这个项目,最大的收获不是多会写一个接口,而是理解了业务状态流转在软件设计里的核心地位。做文物相关的系统,数据一旦入库就意味着责任,所以每一步都必须有迹可循。希望你借鉴这套设计时,也能把“状态可控、操作有痕”这八个字放在最前面。MySQL8.0 那几个时区、认证协议的坑写在这里了,我踩过一遍,你就不用再踩一遍。