FEATURED · 精选文章

Spring Boot + Vue 高校财务报销系统开发实战:审批状态机与经费余额设计详解

发布时间 / 2026/9/9 13:18:23
来源 / 创域科博编辑部
栏目 / 资讯中心
Spring Boot + Vue 高校财务报销系统开发实战:审批状态机与经费余额设计详解 做高校财务报销这类系统之前我一直以为它跟普通的企业OA审批差不多无非是建一张报销单填个金额选个审批人然后一路点通过。真把需求捋完我才发现高校报销单的业务复杂度被严重低估了。经费来源分纵向课题、横向课题、学科建设经费、部门公用经费一张单子可能涉及多个费用明细审批链通常是经办人提交、项目负责人审核、院系领导审批、财务处会计初审、出纳复审才能打款。中间任何一个环节驳回单子就得回到发起人手里修改再重新提。这套流程如果只用简单的CRUD去堆后期改状态流转会改到怀疑人生。这篇文章把整个系统的设计思路、核心表结构、后端状态机设计、Vue前端页面拆分、容易踩坑的联调细节都整理出来。项目基于Spring Boot和Vue前后端分离适合正在做毕业设计、或者刚接触管理系统开发想找个完整实战案例参考的同学。你不需要照着抄每一行代码更重要的是理解“审批状态怎么设计”“经费余额怎么扣”“为什么一张报销单要拆成主表和明细表”这几个核心问题。1. 高校报销业务为什么比普通企业审批更“难建模”先说结论高校财务报销系统的核心难点不在表单而在经费和状态。很多第一次做这类系统的人上来就画表结构把“报销单”设计成一张大宽表字段堆了几十个结果做到审批环节发现状态根本没法维护。这就是典型的没把业务吃透就动手。高校报销跟企业报销最大的差异是“钱从哪来”。企业报销通常走公司统一费用池部门或者成本中心做一个维度就够了。高校不一样老师的科研经费可能是国家纵向课题、企业横向合作、学校双一流建设经费、学院公用经费每个来源的预算科目、使用限制、审批规则都不一样。报销单上必须能追溯到具体的项目经费卡系统里要有项目维度的剩余经费余额提交报销时实时校验“这笔报销会不会把项目经费刷超”。另一个差异是审批链更长。企业OA经常就是直属领导一个节点最多加一个财务。高校普遍是经办人可能是老师也可能是研究生→ 项目负责人 → 院系分管领导 → 财务处初审会计 → 出纳复审 → 打款归档。光状态节点就是六七个而且节点之间存在驳回跳转逻辑。举例来说财务初审驳回的单子是不是一定要退回给经办人还是可以退回给院系领导不同学校的财务制度不一样这套规则必须在设计阶段定死否则后期就是无底洞。还有一点是时间周期。高校报销有明显的“年末冲刺”特征每年11月到12月财务处积压的单据量是平时的好几倍。系统设计时列表查询、统计报表、审批工作台都要考虑数据量增长不能一张表全量刷。我建议在报销单列表页强制分页默认只查当前用户相关的单据统计页用单独的聚合SQL避免大数据量下页面卡死。这套系统的功能边界也跟普通管理系统不一样。除了最基础的报销单填报和审批还要有用户与角色管理、院系部门管理、项目经费管理、费用明细登记、审批历史留痕、报销台账导出。发票附件上传也是刚需因为财务审核需要看到原始票据。学生或老师提交报销单时经常要上传发票照片、付款截图、采购清单这些附件要跟报销单关联存储后端用文件路径或对象存储地址记录不要直接往数据库里塞二进制。2. 技术选型Spring Boot Vue 的组合到底够不够用2.1 后端框架的取舍后端用Spring Boot几乎是这类系统的主流选择Spring Boot的自动配置、起步依赖、内嵌Tomcat让项目搭建成本极低。Controller、Service、Mapper三层结构清晰配合MyBatis-Plus做单表CRUD和分页开发效率比原生MyBatis高很多。用户管理、角色权限这种管理后台标配功能用MyBatis-Plus的Wrapper构造查询条件非常方便不需要手写大量XML。项目规模上高校内部的财务报销系统并发量实际上并不高一台普通服务器加一个MySQL实例完全能扛住没必要上Spring Cloud那套微服务体系。微服务带来的服务注册、配置中心、网关、链路追踪这些复杂度对这个体量的系统来说纯属负担。如果只是毕设或者校内系统Spring Boot单体应用加上合理的数据表设计和索引性能完全够用。2.2 Vue 端选 Vue 2 还是 Vue 3Vue 端我建议根据团队熟悉程度选。如果是从零开始的新项目直接上Vue 3 Element Plus Vite组合式API写起来逻辑复用更舒服构建速度也快。如果网上找的模板或者参考项目是Vue 2 Element UI也不必强行升级Vue 2 生态成熟Element UI的组件对于管理后台这种表单密集型场景完全够用。我实际开发时用的是Vue 3 Element Plus理由有几个Element Plus的表格、表单、弹窗、日期选择器覆盖报销系统几乎所有场景Vite开发服务器热更新很快联调时改前端代码基本秒级刷新组合式API把“查询条件”“表格数据”“分页信息”这种关联状态放在一起维护代码比Options API更紧凑。2.3 为什么不建议直接上 Flowable 工作流引擎很多同学看到“审批流”三个字第一反应是引入Flowable或Activiti。热搜里也经常出现“springboot使用flowable”这类关键词但我明确建议这个项目不要上工作流引擎。Flowable这类引擎是为“流程多变、需要可视化拖拽配置、涉及会签或签、条件路由”的复杂业务流程准备的。高校财务报销的流程虽然链长但拓扑结构非常固定提交、各级审批、驳回、撤回、打款几乎不会出现复杂的并行网关和条件分支。自己用状态字段加一张审批记录表完全能覆盖反而代码更直观出问题好排查。引入Flowable意味着要维护流程定义XML、部署流程、处理流程实例与业务表的关联学习成本和部署成本都会增加对一个小型管理系统来说得不偿失。说实话如果项目是答辩展示自研状态机能讲清楚状态流转的逻辑反而比“导入了Flowable但只会调用API”更能体现你的系统设计能力。2.4 权限控制JWT拦截器还是Spring Security权限这块我走了个弯路。第一版直接引入Spring Security配置了一大堆SecurityConfig、UserDetailsService、过滤器链结果前端联调时跨域和匿名接口放行折腾了很久。后来我换成JWT HandlerInterceptor的方案反而清爽很多。我的做法是登录成功后生成JWT返回前端前端存在localStorageaxios请求拦截器统一在Header里加Authorization后端写一个AuthInterceptor配置放行白名单登录接口、验证码接口其他接口全部校验Token并解析出用户ID和角色存到ThreadLocal里供Service层使用。角色权限的校验在需要的地方手动判断或者用自定义注解加AOP统一处理。这个方案对“接口需要知道当前登录人是谁”这个核心需求满足得非常好。报销单的“我的单据”“待我审批”这些查询都能直接从Token里拿用户ID不需要前端每次把用户ID当参数传过来。3. 数据库建模报销单不是一张大宽表能搞定的3.1 核心表结构数据库设计是这个项目的灵魂。我先给出核心表清单再逐个讲设计理由。sys_user用户表字段包含用户ID、姓名、工号/学号、密码BCrypt加密、所属院系ID、手机号、邮箱、状态sys_role角色表角色枚举大概是经办人、项目负责人、院系领导、财务初审、财务复审、系统管理员sys_user_role用户角色关联表dept_info院系/部门表树形结构支持二级学院下的系所project_info项目经费表字段包含项目编号、项目名称、经费类型纵向/横向/校级、负责人ID、总金额、已用金额、创建时间expense_claim报销单主表expense_detail报销明细表approval_record审批记录表finance_record打款记录表3.2 报销单主表和明细表为什么要拆开这是数据库设计里最关键的一步。一张报销单可能对应多条费用明细比如一次出差报销里面有交通费800元、住宿费1200元、伙食补助400元三条明细分别属于不同的预算科目。如果把明细塞在主表里就会出现一行记录存一个JSON字符串或者拆成多行冗余主表字段查询和统计都很难受。正确做法是拆成主表和明细表。expense_claim存单据级别的基础信息和汇总状态expense_detail存每条明细的费用科目、金额、票据号码、说明。主表上的报销总金额是一个冗余字段在提交或保存时由明细金额汇总后写入。为什么做冗余因为列表页、审批待办页、统计报表都需要展示总金额如果这些地方都实时去明细表SUM数据量上来后查询会变慢。用冗余字段加一个落库时更新总金额的逻辑查询性能好很多。expense_claim核心字段大概是这样主键ID、报销单编号业务编号比如BX20250601001、申请人ID、申请人姓名、所属院系ID、项目ID、项目名称、经费类型、报销摘要/事由、报销总金额、单据状态、当前审批节点、提交时间、完成时间、逻辑删除标记。费用明细表主键ID、报销单ID、费用科目差旅费/办公用品/设备费/测试费/材料费、费用说明、发票号码、金额。3.3 审批记录表全链路留痕是财务系统的底线财务系统跟普通业务系统不一样每一笔审批动作都要有据可查。approval_record表记录每一次审批操作审批记录ID、报销单ID、审批人ID、审批人姓名、审批节点项目负责人/院系领导/财务初审/财务复审、动作提交/通过/驳回/撤回/打款、审批意见、操作时间。界面上的“审批历史时间线”就是查这张表按时间倒序展示。一旦后续财务对账有问题还能回溯到是谁在什么时间做了哪个操作、填了什么意见。需要注意审批记录表的数据只追加不删除、不修改物理删除也不要开放。报销单主表可以做逻辑删除审批记录表从设计上就应该禁止任何删除入口。3.4 金额字段的类型选择金额必须用DECIMAL(12,2)Java侧对应BigDecimal。不要用Float或Double浮点数在金额计算上会有精度丢失问题财务系统里一分钱都不能差。前端传金额时也要注意表单控件拿到的值是字符串提交给后端时要统一转成BigDecimal不要在前端先转Number再传避免大数出现科学计数法问题。4. 后端开发顺序与状态机设计4.1 推荐的开发顺序如果你的目的是快速跑通整套系统我建议按这个顺序开发后端而不是按照表结构从上往下写。第一步做用户登录和JWT拦截器先把“谁能进系统”这个问题解决。 第二步做院系、用户、角色这几个基础管理接口虽然页面在后端开发阶段用不到但后面所有业务都依赖这些基础数据。 第三步做报销单主流程新建报销单、保存草稿、提交审批、根据角色查待办、审批通过、驳回、查询历史。 第四步做经费和统计项目经费余额扣减、报销台账导出Excel。 第五步再补附件上传、通知消息这些增强功能。原因很简单报销主流程是这个系统的骨架骨架通了其他功能都是往上面挂肉。很多同学喜欢先把所有表的CRUD全写完再开始连业务结果做完用户管理、部门管理、菜单管理半个月过去了主流程还没碰时间全耗在非核心功能上。4.2 单据状态怎么设计状态字段我用的是tinyint类型存数字代码配合一套常量类管理。状态枚举大概是0草稿保存过但没提交只有本人可见1待项目负责人审批2待院系领导审批3待财务初审4待财务复审5审批通过待打款6已完成已打款归档7已驳回发起人可编辑后重新提交8已撤回发起人在审批人处理之前主动撤回这些状态是整个系统的核心。后端的每次审批操作本质上就是一次状态迁移。我会把所有合法的状态迁移路径写清楚不允许随意跳转。比如待项目负责人审批的单子只能流转到“待院系领导审批”或“已驳回”不可能直接跳到“已完成”。状态存储还有一个关键点更新时必须带条件。更新的SQL是“update expense_claim set status 下一状态 where id ? and status 当前状态”影响行数为0说明别人已经处理过这条单子直接返回“该单据已被处理”。这个写法能防止并发环境下两个人同时审批同一张单子的情况。4.3 Service层把状态流转封装成独立方法状态迁移的代码我建议封装到独立的Service方法里不要在Controller里写一堆if else。比如approve方法接收报销单ID、审批人ID、审批意见内部先查单子、校验当前状态和权限、判断审批人是否属于当前节点、更新主表状态、插入审批记录、如果是驳回还要处理经费预占的释放。这样每个状态流转的入口都收口到一个方法逻辑清晰测试也好写。4.4 经费余额的扣减时机经费余额处理是个容易出错的地方。我在设计里增加了一个字段占用金额。项目总额减去“已占用金额”才是当前可报销余额。占用金额的含义是“已经提交审批但还没最终打款的金额”。提交报销单时把这笔金额加到项目经费的占用金额上审批驳回或者发起人撤回时释放占用金额打款完成后把占用金额转成已用金额。为什么不能直接扣已用金额因为单子还在审批中金额只是被“锁定”并没有实际花出去。如果提交就扣已用金额驳回后还要回滚逻辑绕而且容易出bug。用占用字段隔离财务的已用金额只记录真正打款完成的单子账目非常清晰。5. Vue 前端的页面拆分与关键实现5.1 页面清单Vue前端我按业务角色拆页面。登录页、工作台、报销单填报页、我的报销单列表、审批待办页、报销台账与统计页、项目经费管理页、系统管理页用户/角色/院系管理。没必要做那种特别花哨的大屏可视化财务系统更重要的是信息密度和信息准确性。5.2 报销单填报页的设计填报页是这个系统前端最复杂的页面。上半部分是报销基本信息报销事由、所属项目用级联选择器选了项目后自动带出经费类型和剩余可用金额、报销总金额。下半部分是费用明细列表支持动态增删行。每一行选择费用科目、填金额、填发票号码、填说明。每次增删行或修改金额页面上的总金额自动重新计算。提交按钮点击时前端先做一次校验明细至少一行、每行金额不能为空且大于0、总金额不能超过项目剩余经费然后才允许调用提交接口。这里有个体验层面的细节状态为“已驳回”的单子进入编辑页时明细数据要完整回显而不是重新填一遍。审批人的驳回意见要在表单上方醒目标出这样发起人知道自己改哪里。5.3 审批工作台怎么设计审批工作台是财务处和院系领导每天打开最多的页面。它不要做得像一个普通的表格列表至少要做到三点第一待办列表默认只显示“待当前角色审批”的单子每条显示报销人、院系、项目名称、金额、提交时间。 第二点击某条待办右侧抽屉弹出单据全貌包括基本信息、费用明细表格、提交人上传的发票附件预览、审批历史时间线。审批人不需要跳转页面就能完成大部分信息核对。 第三抽屉底部放通过和驳回按钮驳回时必须填写驳回意见否则不允许提交。通过操作要求二次确认防止误点。实际使用中财务初审人员每天要审几十单如果每个单子都要点进详情页再点返回效率极低。抽屉式设计能让审批人在列表和详情之间快速切换。5.4 页面路由守卫和按钮权限前端路由守卫主要做两件事未登录用户跳转到登录页已登录用户按角色过滤菜单。菜单通常在登录后根据角色动态生成管理员看到系统管理入口普通教师只看到报销填报、我的单据和经费查询。侧边栏菜单的渲染数据来自后端接口前端根据返回的路由表动态添加。按钮级权限比如“驳回”按钮只有当前状态节点对应角色才显示“提交”按钮只有单据状态为草稿或已驳回时才可用。这些控制都属于体验优化真正的权限校验必须以后端接口为准。6. 最容易返工的三处业务校验6.1 项目经费余额校验的精度问题经费校验的规则是“提交报销时报销总金额不能超过项目剩余可用金额”。剩余可用金额等于项目总金额减已用金额减占用金额。这个校验逻辑看着简单实际容易出问题的点是精度。所有金额运算必须用BigDecimal而且要注意比较时用compareTo而不是equals因为BigDecimal的equals还会比较精度位数2.0和2.00用equals判断是false用compareTo才是数值比较。另外一个容易忽略的点是“重新提交”时的占用金额重复计算。已驳回的单子重新提交时如果上一次提交时已经加了占用金额驳回时也释放了那重新提交再占一次逻辑正确。但如果驳回时释放的时机写错了就会出现占用金额越占越多项目明明还有钱却提示余额不足的诡异问题。我的建议是占用金额的变动统一封装在状态流转方法里不要散落在各处。6.2 驳回之后重新提交的状态清理驳回处理有两种思路一种是驳回后单子变成草稿发起人修改明细后重新提交提单编号不变另一种是驳回后原单作废重新生成一张新单。高校场景下财务处大概率会要求保留原单痕迹所以我选了第一种方案。重新提交时要做的状态清理包括清空之前的审批记录吗不要保留。审批历史是财务追溯的凭据不能删。那审批时间线展示时同一个节点的多次审批记录怎么展示我按时间倒序全部展示第一次项目负责人审批通过、第二次项目负责人审批通过、两次驳回原因都列出来所有动作可回溯。6.3 审批人离职、调动时待办怎么处理这个坑是上线后发现的。院系领导如果调岗了他名下待审批的单子就会一直卡在那里。后来的解决方案是系统管理员在用户管理页面可以执行“待办转移”把某个用户的所有待办单子转给另一个用户。对应的SQL就是“update expense_claim set current_approver 新审批人ID where current_approver 旧审批人ID and status 对应待审批状态”。这个功能虽然不起眼但实际维护中特别重要。你在做系统的时候容易忽略这类后续运维需求但这类功能往往才是用户真正会给你点赞的。7. 前后端联调阶段踩过的坑7.1 LocalDateTime的序列化问题前端传日期时间字符串比如“2025-06-01 14:30:00”后端LocalDateTime字段解析直接报错这是最常见的坑之一。Spring Boot默认的Jackson对LocalDateTime支持不友好。解决办法有两种。一是实体字段加JsonFormat(pattern “yyyy-MM-dd HH:mm:ss”)二是配置一个全局的Jackson自定义序列化和反序列化器。我建议直接配全局不然每个字段都加注解太啰嗦。前端回显时也要确认后端返回的格式是“yyyy-MM-dd HH:mm:ss”如果是带T的ISO格式表格里显示出来很丑需要前端做格式化处理。7.2 空字符串转BigDecimal报错前端表单里金额输入框如果没填提交时默认是空字符串“”后端BigDecimal字段接收时直接抛Can not deserialize value of type java.math.BigDecimal from String value”异常。解决办法是前端提交前把空字符串统一转成null或者后端写一个类型转换配置把空字符串转null。我是前后端都处理了前端校验必填后端做全局的StringToNumber转换兜底。这样即使前端绕过校验直接用Postman调接口也不会因为空串导致500。7.3 axios拦截器401死循环axios响应拦截器里处理Token过期最常见的错误是token失效后调用刷新token接口刷新接口又返回401然后再次调用刷新接口形成死循环。我的处理方式是在拦截器里判断请求URL刷新token的接口不进入重复处理逻辑刷新token后重放原来的请求通过返回Promise重新发起。另外登录页本身的接口要跳过校验否则未登录用户访问系统时会被反复重定向。7.4 跨域配置和拦截器的顺序问题前端在8080端口开发后端在8081跨域是必须处理的。我在后端写了一个CorsFilter允许指定来源、指定Header、指定方法。有个细节是跨域配置必须放在拦截器之前否则预检请求OPTIONS会被拦截器拦截导致前端报“CORS error”而不是真正的接口错误。正确做法是拦截器直接放行OPTIONS请求跨域的具体规则交给CorsFilter处理。7.5 MyBatis-Plus逻辑删除和统计SQL的坑用了MyBatis-Plus的TableLogic逻辑删除后默认查询会自动追加“deleted 0”条件这对单表CRUD很方便。但如果你写自定义SQL做统计比如统计所有项目的经费使用率MyBatis-Plus的自动拼接不会生效需要自己在SQL里手动加deleted过滤条件。我当时做经费统计报表时查出来的已用金额跟实际对不上排查了半天发现就是统计SQL里没过滤已删除的报销单。所以提醒一下自定义SQL一定记得自己处理逻辑删除条件。8. 附件上传与打印功能的实用技巧8.1 附件组件的实现思路发票附件我用Element Plus的Upload组件对接后端接口。后端MultipartFile接收文件存储路径按日期分目录比如“/data/upload/2025/06/”文件名用UUID重命名避免中文文件名乱码问题。数据库表只要记录文件ID、报销单ID、文件原名、存储路径、文件大小、上传时间就行。列表展示和预览时拼接路径返回给前端。不推荐把文件存数据库的BLOB字段虽然能少一个文件目录但数据库会变得臃肿备份恢复也慢。内网部署环境下还要注意文件预览如果用的是nginx代理必须给上传目录单独配置一个访问前缀否则前端拿到的文件地址是404。8.2 报销单打印财务报销单打印需求我建议直接在浏览器端用window.print()加打印样式实现。做一个专门用于打印的页面或区域样式只保留表格和基本信息隐藏按钮和菜单。打印时把报销单编号、申请人、项目、明细、审批历史都打出来。比后端生成PDF要省事很多而且格式调整也直观。如果后续需要在打印件上盖电子章再考虑引入前端生成PDF的方案也不迟。9. 部署方式与后续可扩展的点9.1 前后端分离部署前端npm run build打包后生成dist目录扔到nginx的html目录nginx配置把“/api”开头的请求反向代理到后端服务端口。后端用mvn package -DskipTests打jar包配上systemd服务或者直接用nohup启动。高校内网部署需要注意前端依赖的Element Plus、ECharts这些第三方库必须打包进本地静态资源不能引用公共CDN因为内网环境访问不了外网。构建时用Vite的资源路径配置输出相对路径或绝对路径避免部署在子目录时资源加载失败。9.2 后续可以做的增强如果这套系统要长期用下去有几个增强方向可以参考。第一是导出Excel。报销台账导出用EasyExcel按时间范围和经费类型筛选后导出直接给财务处做月末对账用比页面在线看表格实用得多。第二是消息通知。审批通过或驳回后给发起人发送站内消息避免用户反复刷新页面看状态。用WebSocket做实时未读数提醒体验会提升一大截。第三是统计看板。按院系、按经费类型、按月统计报销金额和单量给财务处做预算执行分析。这类页面查询量大建议单独建统计表或者用定时任务预聚合不要每次都全表扫描。如果让我重新做一遍这个项目我会在项目启动的第一周就把完整的“单据状态流转图”和“经费余额变动图”画出来贴在工位上。状态图理清之前不写一行业务代码。很多返工都是因为开发中途才发现“哦原来驳回之后还能再驳回”这类没想清楚的流程分支。先把业务边界定死后面的开发节奏会顺畅非常多。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻