SpringBoot+Vue3文物征集管理系统:状态机与RBAC权限设计实践

发布时间:2026/10/10 4:19:33
SpringBoot+Vue3文物征集管理系统:状态机与RBAC权限设计实践
1. 一个最不像软件项目的业务场景文物征集为什么需要一套系统先讲一个我亲眼见过的场景真实得有点狼狈。一位负责文物征集的工作人员桌面上摊着四五本手写台账旁边堆着几十个牛皮纸袋纸袋里塞着文物来源人填写的登记表、照片打印件、电话沟通记录。想查一件铜像的征集进度得从第一本台账翻到最后一本中间还要打电话给鉴定组的老师问上次那件东西的结论出了没有。我当时站在旁边脑子里冒出来的第一个念头是这个业务场景缺的不是一个Excel表而是一套能把人、事、物、审批状态全部串起来的系统。红色革命文物征集管理系统这个项目标题看着好像带着一点特殊色彩但从软件工程的角度去拆它本质上就是一个典型的政务/文博类业务信息管理系统。核心业务流程是面向民间征集红色革命文物包括征集线索登记、来源人信息维护、文物基础信息录入、影像资料管理、鉴定评估、征集审批、入库建档、征集台账统计等环节。这个项目选的技术栈非常有代表性SpringBoot2 Vue3 MyBatis-Plus MySQL8.0加上传统的MVC分层。可以说它就是当前Java Web方向最稳妥的组合拳之一。这套组合在企业级项目里出现的频率极高尤其适合做管理信息系统MIS因为它们的定位恰好匹配这类业务SpringBoot负责后端快速启动与依赖管理MyBatis-Plus解决单表CRUD和简易复杂查询Vue3负责前端数据驱动渲染MySQL8.0承担主数据存储。你如果正在学Java Web或者准备做一个毕设/工程项目这个系统几乎可以作为一份标准答案来拆解。整套系统要覆盖的身份角色大概有三类征集员录入线索、提交征集申请、鉴定人员出具鉴定意见、管理人员审批、入库、统计。每个角色看到的界面不同操作权限不同但都围绕同一批文物数据协同工作。所以这系统的复杂度不在于某个单点功能有多难写而在于状态怎么流转、数据怎么关联、权限怎么收敛。这篇文章我不会去贴一张完整的数据库脚本然后让你照抄那没有意义。我更想做的是把整个系统的设计思路、核心代码的组织方式、我在实际开发中觉得值得注意的坑以及这套源码文档最值得借鉴的地方一条条讲清楚。无论你是要拿它做参考、做二次开发还是想学整套架构思路都能从这里找到可以落地的内容。2. 数据模型先行文物征集背后的状态机与核心表结构设计2.1 文物不是普通商品它的数据模型要能承上启下做这类系统第一个要回答的问题是文物这条核心数据到底应该怎么建模红色革命文物可能是书信、勋章、武器残件、证件、生活用品每一件都带有固定的历史信息但同时又有很强的个体差异性。比如一封书信需要记录写信人、收信人、时间、地点一把刺刀需要记录材质、尺寸、保存状况。如果你把所有字段都塞进一张表表会变得臃肿且大量字段为空。我在设计这份项目中的数据模型时采取的思路是主表 扩展表 关联表三层结构。主表就是文物基本信息表存放无论什么类型都需要的通用字段比如文物名称、类别、年代、来源方式、征集状态、存放位置、录入人、录入时间。类别字段控制着这件文物走哪一种鉴定模板。扩展表或者叫文物详情表存放不同类别特有的明细信息用文物主键做外键关联。关联表则负责连接其他业务对象比如文物与影像资料、文物与鉴定记录、文物与捐赠/收购协议。这样设计的直接好处是新增一类文物时不需要改主表结构只需要增加对应的扩展表统计时只需要join主表展示详情时再根据类别去加载扩展信息。MVC模式下每个层次都清晰实体类对应表结构Mapper对应SQL操作Service承载业务逻辑Controller暴露接口。2.2 征集流程状态机比想象中更能决定系统成败所有管理系统的核心难点都是状态。文物征集不是登记一下完事它是一个典型的多节点审批流程线索登记 → 初步审核 → 专家鉴定 → 价值评估 → 征集审批 → 入库或退回。有些单位还会把来源人协商和协议签订单列为独立状态节点。我在项目里用一张字典表加一个状态字段来控制整个生命周期。状态字段存放在文物主表里用字符串类型存状态编码比如REGISTERED、PENDING_REVIEW、UNDER_APPRAISAL、PENDING_APPROVAL、DONATED、REJECTED。字典表则维护每个编码对应的显示名称和当前阶段序号前端拿到字典后可以动态渲染标签颜色和流转按钮。这样做比直接写死数字的做法灵活得多。因为征集流程涉及到多部门和多个角色状态含义可能会调整。比如有的单位希望在鉴定和评估之间再加一个补充材料节点那我只需要在字典表和前端路由映射里增补记录业务代码和数据库结构都不需要大动。下面我把这套状态机用表格梳理一下开发时可以直接拿去做参照状态编码显示名称前置状态允许操作角色流转结果REGISTERED线索已登记无征集员提交初审PENDING_REVIEW待初审REGISTERED征集员/管理员通过进入鉴定或退回UNDER_APPRAISAL鉴定中PENDING_REVIEW鉴定人员出具鉴定报告PENDING_APPROVAL待终审UNDER_APPRAISAL管理员通过则入库不通过则退回APPROVED入库完成PENDING_APPROVAL系统触发生成库房编号REJECTED征集不通过任意待审状态管理员流程终止这张状态表不是随便列的它直接映射到代码里的state字段以及 Service 层的方法命名。比如提交初审就是submitReview(id)只有REGISTERED状态的文物才能调用这个方法否则抛业务异常。前端按钮的显隐则完全由当前状态决定后端再做二次校验防止有人绕过前端直接调接口。2.3 档案材料关联让文物活得像一个人文物系统里经常被忽略的是影像和文档资料。实物文物尤其红色革命文物往往伴随大量历史档案来源人提供的证明、旧照片、信件扫描件、媒体报道、专家手写鉴定意见等。这些东西如果只作为base64存进数据库性能会迅速恶化数据库表也会变成垃圾场。我建议的处理方式是文件本体存放在服务器磁盘或对象存储数据库里只记录文件名称、存储路径、大小、MIME类型、所属业务类型。我在这个项目的表设计里专门划了一张file_record表核心字段包括业务模块如relic、appraisal_report、业务表主键、文件URL、排序号、上传人、上传时间。这样任何一类业务需要挂附件都走同一套上传逻辑不用每个业务各写一套。前端的Vue3组件也统一封装一个FileUpload组件内部调用后端上传接口成功后把返回的路径塞进表单的files数组里。2.4 权限模型三张基础表的RBAC实现权限部分我见不少项目喜欢堆各种复杂的框架实际上对于这种单人开发/小团队维护的MIS系统经典的RBAC基于角色的访问控制就够了。核心表就三张用户表、角色表、用户角色关联表。登录用户拿到角色以后在Java后端通过拦截器或AOP判断接口权限。菜单权限则通过前端路由守卫和动态路由控制后端返回该角色可见的菜单树前端用Vue Router的addRoute动态注册。角色我分了三档征集员、鉴定人员、系统管理员。征集员只能访问线索登记和文物列表鉴定人员只能看到待鉴定和鉴定中文物管理员拥有全部权限并负责终审入库。粒度再细分一点可以在角色表和接口之间建一张权限点关联表不过对于这个业务体量来说没有太大必要角色到接口的白名单已经足够安全。3. 后端落地SpringBoot2与MyBatis-Plus的选型逻辑与实现细节3.1 工程骨架与依赖为什么这套组合不会过时后端工程结构我没有采用网上很多教程里那种一键生成CRUD的简单模板而是按照MVC分层的标准来做controller包、service包、mapper包、entity包、common包、config包。工具类单独放utils包异常统一放进handler包。这样哪怕你在SpringBoot3最火热的今天回头看这个结构依然通透。为什么用MyBatis-Plus而不是MyBatis原生或者Spring Data JPA这是我的经验之谈这个系统的单表CRUD占比非常高而且涉及大量selectById、selectPage、条件更新。MyBatis-Plus的BaseMapper能把这些方法全部自动实现代码量能减少一半以上。团队里如果有新手学习门槛也比另一种方案低不少。而标量查询如统计各类文物数量、按月度统计征集增量则通过Select注解手写SQL不滥用Wrapper构造复杂条件。核心依赖大概这样dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.x/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency3.2 后端核心表映射与业务Service设计我以文物主表实体举例展示一下实体设计的风格每个字段加了JSR 303校验注解比如名称字段NotBlank年代字段NotBlank类别字段NotNull状态字段不可为空。MyBatis-Plus的TableName注解指明表名TableId指定主键策略。这里特别注意一个细节主键策略我选的是IdType.ASSIGN_ID也就是雪花算法而不是数据库自增。原因是文物数据未来有可能需要做分库迁移雪花ID能最大限度避免ID冲突而且前端还能用ID参与排序。Service层我遵循接口实现类的写法RelicService接口定义业务方法RelicServiceImpl里写实现。这样做的好处事务注解可以加在实现类方法上便于后续做切面日志、权限校验。比如提交征集申请这一步需要在一个事务里同时更新文物的状态字段和插入一条审批记录如果后者失败前者必须回滚Transactional(rollbackFor Exception.class)是必须的。3.3 统计报表SQL怎么写才能扛住大数据量文博类系统到了一定规模不可避免要做统计。年报、季报、分类统计这些功能如果全部用Java代码去查出来再循环算性能会非常感人。我在项目中把所有统计类需求都下沉到SQL里用聚合函数一次查出结果Java侧只做数据的组装和格式化。举个例子统计每个月各类文物的新增数量我用一条SQL搞定SELECT DATE_FORMAT(create_time, %Y-%m) AS month_key, category, COUNT(*) AS total FROM relic WHERE create_time BETWEEN #{startTime} AND #{endTime} GROUP BY month_key, category ORDER BY month_key DESC;然后在Mapper接口里声明一个ListRelicCountVO countRelicByCategory(String startTime, String endTime)返回结果直接映射到VO对象。前端拿到这个数组用Vue3的图表组件画柱状图或折线图整个统计功能写下来最多百来行代码。如果数据总量超过百万行可以考虑在create_time上建普通索引查询范围尽量限制在三个月以内。3.4 后端踩坑实录状态流转时的并发安全我要特别提醒一个我在项目调试过程中踩到的坑状态机的并发问题。假设一个征集员在列表页重复点击提交初审按钮前端虽然做了按钮loading限制但如果有人通过接口工具直接并发请求两次后端的Service层就有可能出现文物状态被连续更新的问题。第一次从REGISTERED改成PENDING_REVIEW第二次因为当前状态已经不再是REGISTERED业务校验肯定过不去。但如果校验和更新不是原子的还是有极小概率在并发场景下出问题。我的解决办法是在更新语句上加状态条件boolean updated relicMapper.update( new LambdaUpdateWrapperRelic() .eq(Relic::getId, id) .eq(Relic::getStatus, RelicStatus.REGISTERED.name()) .set(Relic::getStatus, RelicStatus.PENDING_REVIEW.name()) ) 0; if (!updated) { throw new BusinessException(文物当前状态不允许此操作请刷新后重试); }加上这个条件后即使并发请求进来MySQL的行锁也会让第二次更新匹配不到记录返回影响行数为0从而保证状态不会跳变。这是状态机类业务里最核心的一层保障比单靠前端按钮限制靠谱得多。4. 前端交互Vue3 Element Plus的审批流转设计4.1 工程结构与状态管理前端我选的是Vue3的组合式API配合Vite构建UI库用Element Plus。项目管理上views目录按业务模块拆分子目录relic、appraisal、statistics、system。router目录用懒加载方式引页面组件避免首屏一次性加载所有js导致响应变慢。store目录是Pinia的实例主要用来存当前用户信息、权限点、菜单树。按需引入Element Plus组件后构建产物体积能控制在合理范围内。Vue3最舒服的一点是Composition API的ref和reactive让表单和列表页的代码变得非常规整。比如列表查询页我用一个queryParams响应式对象承载所有搜索条件调用后端接口时直接把queryParams作为参数序列化传过去。分页组件绑定total和currentPage每切换页码就重新调一次接口。整体代码保持在一种数据流单向的模式界面操作修改参数触发的请求请求后拿到的数据再回流到页面响应式变量。4.2 一个带有记忆点的多步骤表单文物征集录入表单是整个前端里最多字段的地方不可能用一页堆完所有输入项。我把它拆分成了来源人信息文物基本信息影像资料备注说明四个步骤用Element Plus的el-steps组件做步骤条提交时一次性把四个步骤的所有字段打包成JSON发给后端。这样用户的心理负担会小很多录完一个来源人信息后后面文物信息可以稍缓不至于看到二十几个字段就想关页面。表单里的类别字段用得是动态联动。用户先在文物分类下拉框里选择类别然后表单区才会出现与该类别关联的扩展字段。这个判断逻辑在前端维护一份字段配置映射后端提供一个接口返回对应类别的字段定义。极端情况下某些小单位不需要扩展字段全部走通用文本字段兜底。4.3 审批进度时间线把状态机可视化状态机设计得再好如果用户看不到文物走到哪一步了系统的易用性就大打折扣。我参考了很多文博类同类系统的做法在文物详情页放了一条时间线组件。数据来源于approval_record表每次状态变更都会插入一条记录内容包括操作人、操作动作、操作时间、备注说明。前端用el-timeline组件渲染配合状态标签的颜色变化用户打开详情页一眼就能看出这件文物从线索登记到现在经历的完整环节。这条操作记录表还有一个隐藏价值它可以作为审计日志使用。当内部出现了某件文物被谁修改了状态、改完之后评价不高这种疑难问题只要拉出时间线责任链条一清二楚。4.4 权限在前端的落地方式前端权限我用最直接的方式实现路由守卫里比对动态菜单。登录返回的数据里包含当前用户的角色和可访问菜单标识数组。router.beforeEach里如果访问的路由标识不在菜单数组中直接跳转到403页面。Element Plus的侧边栏菜单也根据同一份数据进行渲染。按钮级别的权限比如征集员不显示通过终审这个按钮我没有做太复杂的自定义指令而是直接在模板里用v-ifhasPermission(relic:approve)控制。hasPermission方法从Pinia中读取当前用户权限点列表并返回布尔值。有些项目把按钮权限做成指令v-permission会更简洁我之所以没用指令而用v-if是因为这个系统里需要控制的按钮数量不多直接用方法最快也好懂。5. 权限边界与业务严谨性管理者、鉴定者、征集者三个角色的隔离5.1 角色业务边界的可用性设计这类系统的权限不只是登录后能不能访问某个页面的问题还涉及数据本身能不能被看到。比如征集员录入了线索鉴定人员只需要看到待鉴定文物并不需要看到征集员和来源人聊天的记录。管理员查看列表时数据范围是整个库征集员只应该看到自己录入的那几条。所以除了RBAC数据权限也要跟着角色走。我在Mapper层对征集员做了一条硬约束列表查询方法接收userId参数当角色是征集员时Service层强制给查询条件追加eq(Relic::getCreatorId, userId)。管理员则不追加该条件。这样即使前端传递了伪造参数后端也无法越权查询。这也是我对权限这件事的一个原则前端控制只是体验优化真正的安全边界永远在后端。5.2 鉴定人员的独立性防止既当运动员又当裁判鉴定这个环节需要一定的独立性。管理员可以审批、入库但不能代替鉴定人员修改鉴定结论和评分。所以在权限设计上鉴定的接口只开放给鉴定角色管理员即使拥有后台管理权限也不得直接调用鉴定接口修改结论。我甚至在后端写了一个专门的方法校验当前登录用户是否属于鉴定组角色。这个设计可能在开发时看起来有点多余但上线后会少很多无谓的内部纠纷。我曾经见过一个同行做的系统因为管理员能直接改鉴定报告导致一件文物后续被上级单位质疑征集的合理性和鉴定结论的可信度。所以在文物类系统里业务严谨性远远比功能开发快重要。代码层面做的隔离机制包括鉴定结论写入单独表、更新需提供鉴定人ID、修改历史记录留痕。5.3 登录安全Spring Security还是轻量拦截器我评估过在这个项目里引入Spring Security后来决定暂时用一套轻量的JWT 拦截器方案。原因很坦白Spring Security的过滤器链路对一个二次开发/教学项目来说配置成本略高而这套系统的真实威胁模型并不需要OAuth2、SSO这类复杂认证协议。具体实现是登录接口验证账号密码后生成一个包含用户ID和角色信息的JWT有效期设为2小时前端把token存到localStorage后端写一个AuthInterceptor拦截所有/api/**请求从Header取token并解析放入ThreadLocal上下文业务代码通过UserContext.getCurrentUser()获取当前登录人。密码校验用BCrypt加密存储MySQL里永远不出现明文密码。一个小提醒如果在生产环境上线建议把token有效期缩短并增加刷新机制同时服务端维护一份token黑名单以应对用户注销后token仍可用的问题。我这套当前处于教学/演示级别的系统暂时不做黑名单但代码里留了扩展点注释也写清楚了。5.4 导入导出与批量操作的安全考虑文博类业务免不了做Excel引入。来源人信息、文物清单经常以Excel形式从外单位交接过来。我用EasyExcel做Excel导入导出导入时后台逐行校验必填字段和格式遇到错误行时汇总成错误列表一次性返回给前端。前端拿到错误后渲染一个table弹窗让用户看到第几行哪个字段出了问题。导出则直接查询出全部符合条件的数据流式写入Excel并下载。这里需要控制的是导出数量我设置了一个上限超过五万行提示用户缩小查询范围避免一次性导出导致内存溢出。6. 源码文档与落地复盘这套系统最值得参考的三个部分6.1 文档里最容易被忽视的运行说明项目标题里写了【含文档】我在拿到源码之后第一件事就是看README和数据库脚本。很多系统源码质量不错但README只有三行字连MySQL的初始化账号和密码都没写这会让第一次运行的人困在第一步。这个项目的文档做到了三件我认为很关键的事第一件事把环境版本列清楚。Java 8、Maven 3.6、Node 16、MySQL 8.x各是什么版本哪个版本区间能跑都写得明明白白。第二件事数据库初始化脚本拆成了两个文件schema.sql负责建表data.sql负责插入基础字典数据和初始管理员账号。这样即使不用Flyway也能手动执行脚本完成初始化。第三件事把后端和前端的启动顺序以及常见端口冲突解决办法写出来了。多了这三点项目第一次跑通的概率会大幅提升。6.2 最值得复制的打包与部署配置这里分享一段我在部署这套系统时的配置心得。后端打包用Maven的spring-boot-maven-plugin执行mvn clean package后生成一个可执行的jar包用nohup java -jar xxx.jar log.out 21 命令启动。前端执行npm run build后把dist目录里所有静态文件交给Nginx托管Nginx通过location /api将请求反向代理到后端的8080端口。如果是学习用直接把前后端都跑在本地就能调试如果是给单位做小规模部署一台2核4G的云主机跑这套系统完全足够。唯一要注意的是MySQL的max_connections默认值很小如果并发不高就不用管文档里也建议了调大到500左右的参数具体要根据服务器的内存定。6.3 源码之外这套方案如何迁移到其他管理场景我从这个项目里最大的收获并不是某个代码片段而是它验证了一套可复用的管理信息系统方法论。文物征集可以抽象成核心业务对象文物 状态机征集流程 角色权限三类角色 流程留痕审批记录。把这个方法论往下套你会发现它同样适合其他类似场景业务场景核心业务对象状态机流程角色划分科技馆藏品征集科技展品线索登记→鉴定→审批→入库征集员/鉴定员/管理员档案室档案接收档案卷宗移交→清点→编目→入库接收员/编目员/管理员图书馆文献捐赠图书文献登记→查重→估价→接收读者/馆员/馆长换句话说你只要把表名、字段名、状态枚举改一改用同样的MVC分层、同样的Vue组件体系就能撑起一个全新的管理系统。这也是为什么我强调不要把注意力只放在这个系统具体做了什么功能上而要看它的数据模型和流程建模思路。这些都是骨架功能只是血肉骨架对了后面怎么长都顺。我在实际运行这套系统的过程中还有一个体会状态机的枚举值尽量不要从1、2、3这种纯数字开始因为一旦中间插入新状态数字顺序就断了用带有业务含义的字符串编码比如UNDER_APPRAISAL更利于代码阅读和后续扩展。另外凡是需要前端展示的后端返回字段一定要统一命名成驼峰风格否则写前端的人会非常痛苦。最后想多说一句如果你准备拿这套系统做二次开发或者是课程设计建议不要只在原封不动跑通后交差试着去改一处状态机的流转逻辑或者加一个统计维度。把代码改坏然后再修回来的过程往往比照着源码抄一遍更值钱。我自己的建议是第一遍跑通整个流程第二遍断点调试主链路第三遍自己动手实现一个类似的小模块。到第三遍结束这套技术栈才算真正长在你自己的知识体系里。