ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java+SSM+Flask科研经费管理系统:从设计到部署全解析

Java+SSM+Flask科研经费管理系统:从设计到部署全解析 上周一个学弟抱着一堆资料来找我一个基于JavaSSMFlask的科研经费管理系统说是带源码、LW论文、调试文档和讲解结果解压缩后对着目录发愁——先启动哪个数据库脚本在哪里为什么登录进去经费数据总是空我帮他理了两小时发现这个项目并不是简单的一个war包丢进Tomcat就完事它的SSM主业务和Flask辅助服务之间是需要联调的。这篇文章我就把从需求拆解、技术选型、数据库设计到部署排错的整个思路完整写一遍给正在做类似毕设或实际项目的朋友一个参考。1. 经费管理的真实痛点不是记账是“预算不超支、审批可追溯”先说需求。科研经费管理系统听上去像是做一个记账本但真正跑过业务才知道科研经费管理最头疼的从来不是“记录支出”而是两件事预算不超支、审批可追溯。一个科研项目从立项开始会划分出设备费、材料费、差旅费、测试化验加工费、劳务费、出版文献费等好几个预算科目。每个科目下又有几十上百笔支出。传统方式是Excel登记加纸质报销单结果就是根本不知道某个科目还剩多少钱报销单审批到哪一步没人说得清财务审计时拿出一堆发票却对不上账。所以系统第一个核心目标就是对预算做刚性控制——任何一笔经费申请或报销都必须先校验该科目剩余额度超了就不能通过。第二个核心目标是审批流程留痕。科研人员提交经费申请项目负责人初审科研秘书复核财务处终审每一步审批人都要看到之前的意见系统里要有完整的审批记录。这样才能回答“这笔钱为什么花了”“谁批的”“什么时候批的”这类灵魂追问。1.1 角色与权限设计这种系统天然是多角色的我在项目中用的是RBAC模型五类角色角色核心权限科研人员申请人查看项目、录入经费申请、提交报销单、查看审批进度项目负责人审批本组申请、查看项目经费执行率、调整组内人员权限科研秘书审核费用合规性、驳回不规范申请、导出报表财务处终审报销、确认支付、生成记账流水系统管理员维护用户、角色、菜单、系统参数不参与业务审批权限拦截不要只依赖前端隐藏按钮我在SSM的后端写了一个拦截器按URL路径匹配角色编码核心操作一律后端校验。因为经费报销金额一旦被绕过系统等于白做。具体拦截规则大概是/apply/**需要科研人员或项目负责人/approve/**需要项目负责人、科研秘书或财务处/system/**仅限管理员。1.2 功能模块清单这里我直接把项目的功能模块整理成一个清单给后面开发时做参考项目管理项目立项、项目结题、项目基本信息维护包括经费总额、起止时间、项目成员。预算管理按科目录入预算、预算调整需要审批、预算执行情况查询。经费申请科研人员发起用款申请系统自动校验科目剩余额度走审批流。报销管理报销单录入、明细行维护、发票附件上传、多级审批、驳回与退回。统计报表项目经费执行率、科目支出对比、部门经费汇总、年度报表导出。系统管理用户管理、角色管理、菜单管理、操作日志。这些模块之间不是孤立的。经费申请通过后会“冻结”预算额度报销单审批通过后再把“冻结”转为“已使用”。报表的数据来源又是预算表和报销表的实时汇总。所以数据库设计必须把这条链路打通而不是各自存一张表就算完事。2. 技术选型不是炫技SSM主业务与Flask辅服务的分工边界这类项目标题里同时出现Java、SSM和Flask很多同学会蒙到底是用Java还是Python其实在我做这个项目时是把它们理解为两个技术栈协同工作SSM负责核心业务Flask负责辅助服务。2.1 核心业务为什么用SSMSSM是Spring SpringMVC MyBatis的组合Spring管对象生命周期和事务SpringMVC接收HTTP请求MyBatis做数据库持久化。这个组合放在经费管理系统上非常合适经费审批涉及多次数据库状态变更必须保证事务。Spring的Transactional声明式事务比手工提交可靠得多。金额和预算这种核心数据用Java强类型加BigDecimal处理能避免Python弱类型可能带来的精度隐患。毕设场景下很多课程考核的就是SSM框架的掌握程度用SSM写主业务答辩时也更容易讲清楚。有人会问为什么不用Spring Boot这是个好问题。但很多院校的毕业设计题目就是明确指定SSM项目源码也延续了这套结构。从复现的角度说SSM项目跑在Tomcat里配置清晰适合教学。如果你自己接项目完全可以换成Spring Boot但代码结构要相应调整这里不展开。2.2 Flask到底承担了什么Flask在这个项目里不是用来抢主业务的它承担了“数据统计报表”和“发票附件解析”这两类辅助任务。先说统计报表。经费管理系统最后总要给领导和财务看报表按项目统计执行率、按科目统计预算使用比例、按院系统计年度经费情况。如果全部用Java写Excel导出和图表生成POI的操作代码非常冗长而且图表渲染还得依赖前端。Python生态的工具链更顺手后端拿到聚合数据后可以直接用pandas快速清洗用openpyxl导出Excel用matplotlib生成图片。Flask只需要把这些结果封装成HTTP接口给Java调用就行。再说发票附件解析。科研经费报销经常要上传发票、行程单、付款凭证这些附件。Java负责文件上传存储但要从附件中提取“金额、开票日期、商户名称”这些关键信息用Python的文本处理和OCR方案更灵活。我在项目里用Flask做了一个/api/reimburse/parse_invoice接口接收文件后返回结构化数据再回填到报销单表单中省去手动录入的麻烦。2.3 两个服务怎么通信我采用的模式是SSM主服务直接访问MySQLFlask辅助服务通过HTTP接口与SSM或前端协作。具体来说报表场景下Flask只读取数据库中的预算表、报销表和流水表计算完指标后把JSON返回给前端附件解析场景下Java先把文件存到本地或OSS再用HTTP调用Flask接口把解析结果写回业务表。这里分享一个实际经验不要让Flask去写核心业务表尤其不要让它写报销单、审批记录、预算流水。否则一旦Flask和Java对同一张表的理解不一致数据就容易乱。Flask只做“读数据、算指标、解析文件、返回结果”这些无状态操作核心事务仍然由SSM在单库事务内保证这样既拿到了Python的便利又不牺牲数据一致性。两个服务在本地调试时的端口要区分开比如SSM跑在8080Flask跑在5001。前端页面通过配置好的接口地址分别调用跨域问题用flask-cors解决。在部署时可以在Nginx配反向代理统一入口。2.4 如果你拿到的源码是“两个版本”网上下载的很多同名项目标题写着“JavaSSMFlask”实际上可能是同一套系统用SSM和Flask各做了一版并不是混合架构。看目录就能识别如果项目里只有一个fund-admin工程并且有spring、mapper这些包这就是纯SSM版本。如果项目里只有一个app.py和templates目录那就是纯Flask版本。如果项目里有java-ssm和flask-service两个目录才是我上面说的混合架构。整篇文章我按混合架构来写因为这种结构最能解释标题里两个技术栈同时存在的合理性。你要是拿到了纯SSM或纯Flask的版本后面的功能逻辑一样可以参照只是少了联调部分。3. 数据库设计成什么样才能让每一笔经费都说得清经费管理系统最怕“账对不上”所以数据库设计的原则不是字段多而是可追溯。我设计了下面这些核心表实际建表时可以按这个思路扩展。3.1 核心表概览表名作用关键字段sys_user用户表id、username、password、real_name、role_id、dept_idsys_role角色表id、role_code、role_name、descriptionproject_info科研项目表id、project_code、project_name、leader_id、total_budget、start_time、end_time、statusfund_budget经费预算科目表id、project_id、budget_category、total_amount、used_amount、freeze_amountreimburse_apply报销申请主表id、apply_no、applicant_id、project_id、budget_id、total_amount、status、apply_datereimburse_item报销明细表id、reimburse_id、item_name、item_type、amount、invoice_no、attach_pathapproval_record审批记录表id、apply_id、approver_id、action、comment、create_timefund_ledger经费流水表id、project_id、budget_id、change_type、amount、balance_before、balance_after、relate_no、create_time、creator其中fund_budget的三个金额字段很有讲究。total_amount是预算总额used_amount是已经报销使用的金额freeze_amount是当前处于审批流程中、尚未实际支付的冻结金额。这样设计的好处是一个科研人员提交了报销单金额会先冻结报销单还在审批时预算额度不会被其他申请占用。等审批通过后再把冻结金额转成已使用金额。整个过程都有账可查。3.2 预算冻结与扣减逻辑实际执行时预算扣减不是简单的“申请通过就扣used_amount”。我建议按两步走申请提交时冻结报销单进入审批流时在fund_budget.freeze_amount上增加申请金额。审批通过后转已使用终审通过那一刻在一个事务里同时更新freeze_amount减掉金额、used_amount加上金额并写入一条fund_ledger流水。这样做还有一个好处就是如果报销单被驳回只需要把冻结金额调回去即可不用动已使用金额。冻结操作要防止并发问题两个用户同时提交报销单都读到剩余额度1000元都够用结果实际支出变成2000元超支。解决方法是查询预算行时加数据库行锁。SELECT budget_id, total_amount, used_amount, freeze_amount FROM fund_budget WHERE budget_id #{budgetId} FOR UPDATE;在Java里这个查询要放在Transactional事务方法中确认冻结后立即执行更新。另一个更轻量级的方案是给fund_budget表加一个version字段做乐观锁更新时带上版本号。但经费场景我更推荐行锁因为预算行的并发冲突是真实存在的行锁逻辑直观还能防止重复提交。3.3 流水表的价值不推荐在业务表里直接改金额后就不管了。这个项目里我坚持所有资金变动都写fund_ledger流水包括预算调整、申请冻结、审批通过、驳回解冻、报销支付、项目结题。流水表里记录变动前后的余额以及关联的业务单号。这样当财务质疑某笔账目时顺着流水表就能反推出每一笔金额是怎么变化的。比如设备费预算50万流水会显示立项初始余额50万某次预算调整剩余变成45万某笔报销冻结后剩余42万报销通过后已使用增加3万。全链路清晰。4. 核心功能从0到1申请、审批、报销和统计报表的落地细节数据库设计好后核心功能的开发实际上就是围绕状态机、事务和查询做文章。我挑几个最考验细节的地方展开说。4.1 审批状态机的实现经费申请和报销单都有状态流转。我在项目里用枚举定义状态避免代码里到处写魔法数字public enum ReimburseStatus { DRAFT(0, 草稿), SUBMITTED(1, 待部门审批), DEPT_APPROVED(2, 待财务审批), FINANCE_APPROVED(3, 已通过), REJECTED(4, 已驳回), CANCELLED(5, 已作废); private final int code; private final String description; // ... 构造方法 }状态流转规则如下当前状态操作结果状态草稿提交待部门审批待部门审批部门通过待财务审批待部门审批部门驳回已驳回待财务审批财务通过已通过待财务审批财务驳回已驳回提交报销单的Service方法核心逻辑是这样Transactional public boolean submitReimburse(Long reimburseId, Long userId) { ReimburseApply apply reimburseMapper.selectById(reimburseId); if (apply null || apply.getStatus() ! ReimburseStatus.DRAFT.getCode()) { return false; } // 校验预算剩余额度是否足够 if (!budgetValidator.checkBudget(apply.getBudgetId(), apply.getTotalAmount())) { throw new BudgetNotEnoughException(该科目预算额度不足); } // 冻结额度 budgetMapper.freezeAmount(apply.getBudgetId(), apply.getTotalAmount()); // 更新状态 apply.setStatus(ReimburseStatus.SUBMITTED.getCode()); apply.setCurrentApprover(nextApprover(apply.getDeptId())); return reimburseMapper.updateStatus(apply) 0; }这里最关键的是Transactional。因为冻结预算和更新状态必须同生共死操作中间任何一步失败都要回滚否则账目就平不了。4.2 报销单明细与Flask附件解析报销单是一主多从结构。主表记录总金额和状态明细表记录每一笔钱的用途。科研人员填报销单时经常要逐行添加“差旅费-高铁票500元”“材料费-试剂2000元”这样的明细还要上传发票。文件上传和解析的流程我是这样设计的Java端接收上传文件保存到指定上传目录文件路径回填到reimburse_item.attach_path。用户点击“识别发票信息”按钮Java端把文件路径或文件二进制通过HTTP POST转发给Flask服务。Flask接口解析出金额、日期、商户名返回JSON。Java把JSON数据回填到报销明细表单用户确认后保存。Flask的接口可以长这样from flask import Flask, request, jsonify app Flask(__name__) app.route(/api/reimburse/parse_invoice, methods[POST]) def parse_invoice(): file request.files.get(file) if not file: return jsonify({code: 400, msg: no file}) # 实际项目中这里会调用OCR或解析工具 # 演示时直接返回模拟结果 result { invoice_amount: 1200.00, invoice_date: 2025-03-18, merchant: 某科技有限公司 } return jsonify({code: 200, data: result})如果只是做课程设计不想引第三方OCR也可以退一步Flask只做PDF转图片预览金额由用户手填不搞全自动识别。重点是把Flask这个服务单独跑通、让Java能调用到答辩时这就是一个亮点。4.3 预算校验的并发与金额精度问题经费相关的数值字段在MySQL里我用的是DECIMAL(12, 2)在Java对象里用BigDecimal坚决不用double/float。浮点数在金额运算上会有精度丢失哪怕是9.99这种数字都可能出现9.98999999。你要是用浮点数算预算等到月末对账的时候就会收获一堆“差两分钱”的抓狂体验。并发超支的问题在前面提过核心解决方案是行锁。真正的代码里还有一道防线即使业务层锁漏了数据库也会通过预算表上的used_amount freeze_amount total_amount这样的约束兜底。不过这种表级约束在MySQL里要用触发器实现比较麻烦所以我更推荐把并发控制老老实实写在事务方法里再用流水表记录每次变动事后能查。4.4 统计报表的几种常见SQL统计报表最核心的指标是“经费执行率”也就是已使用金额加冻结金额占预算总额的比例。一个常用SQL是这样SELECT b.budget_category, b.total_amount, b.used_amount, b.freeze_amount, (b.used_amount b.freeze_amount) AS occupied_amount, ROUND((b.used_amount b.freeze_amount) / b.total_amount * 100, 2) AS execute_rate FROM fund_budget b WHERE b.project_id #{projectId} ORDER BY b.budget_category;跨项目的汇总报表则按院系或年度分组。前端拿到这些JSON数据后我推荐用ECharts画柱状图、饼图和折线图。Flask不需要参与图表渲染只需要提供数据接口这样能减少Python侧的工作量。但是Excel导出这种需求用Flask很舒服因为openpyxl生成一个带合并单元格、表头样式的汇总表只要十几行代码Java的POI写同样效果少说要几十行。5. 把整套源码跑起来部署环境、配置修改与联调排错实录很多同学卡在“跑不起来”这一步。这不怪他们因为这类项目往往没有把运行环境讲清楚。下面是一套能落地的操作流程。5.1 目录结构先看懂一个典型的项目解压后大概长这样fund-system/ ├── java-ssm/ # SSM主工程 │ ├── src/main/java │ ├── src/main/resources │ │ ├── spring/ │ │ ├── springmvc/ │ │ ├── mybatis/ │ │ └── jdbc.properties │ ├── src/main/webapp/ │ └── pom.xml ├── flask-service/ # Flask辅助服务 │ ├── app.py │ ├── requirements.txt │ └── config.py ├── sql/ │ └── fund_db.sql ├── docs/ │ ├── 论文LW.doc │ └── 调试文档.md └── README.md没有README的话就看docs里的调试文档里面一般会写清楚启动顺序和账号密码。5.2 环境准备基础环境四件套JDK 1.8别图新用17老项目大概率跑不起来Maven 3.6Tomcat 8.5或9.0MySQL 5.7或8.0Python 3.8先导入数据库。用Navicat或命令行执行mysql -u root -p -e CREATE DATABASE IF NOT EXISTS fund_db DEFAULT CHARACTER SET utf8mb4; mysql -u root -p fund_db sql/fund_db.sql然后修改java-ssm中的jdbc.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/fund_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password你自己的密码如果源码里用的是com.mysql.jdbc.Driver而你的MySQL是8.x驱动会连不上必须换成com.mysql.cj.jdbc.Driver。5.3 启动Flask服务进入flask-service目录创建虚拟环境并安装依赖cd flask-service python -m venv venv # Windows: venv\Scripts\activate # Mac/Linux: source venv/bin/activate pip install -r requirements.txt python app.py看到* Running on http://127.0.0.1:5001说明Flask起来了。如果端口被占用在app.py里改端口。Flask服务启动不会影响SSM主服务但页面上的报表和发票解析功能就依赖这个服务。所以联调时一定要保持Flask在运行。5.4 启动SSM主服务在java-ssm目录下mvn clean package生成了war包后丢到Tomcat的webapps目录启动Tomcat。如果是在IDEA里运行直接配置Tomcat并添加Deployment把项目的war包挂上即可。访问http://localhost:8080/fund-system/应该能看到登录页。管理员账号密码通常在SQL脚本中初始化或者写在README/论文里比如admin / admin123。如果登录后项目列表为空先检查两个地方数据库连接是否成功。看Tomcat的logs/catalina.out有没有报异常。fund_db里是否导入了演示数据。有些版本为了减少SQL体积只建了空表需要自己补数据。5.5 联调排错实录最常见的联调问题有三个跨域报错、接口地址写死、数据库时区异常。跨域可以用flask-cors解决在Flask初始化时加一行from flask_cors import CORS CORS(app)接口地址写死的情况搜索Java源码中的localhost:5001或127.0.0.1:5001统一改成配置项。否则别人换台电脑还是得改代码。数据库时区异常最典型的现象是启动时抛The server time zone value ...的异常解决办法就是在jdbc.url后面加serverTimezoneAsia/Shanghai。调试时一个很管用的习惯是先用Postman直接测Java接口和Flask接口确认各自返回正常再从前端页面联调。这样能快速定位问题是前端还是后端。6. 源码之外的交付物LW论文、调试文档和讲解怎么准备标题里除了源码还有“LW调试文档讲解”。这几个东西是很多同学最容易忽略又最拉分的部分。一个能跑的系统只是及格把交付物做扎实才是真正的优势。6.1 LW论文不要写成一堆代码论文LW要解决的问题是“为什么这样设计”而不是“代码怎么敲”。我见过很多学生把核心代码往论文里一贴就完事老师看了也头痛。比较合适的论文结构是绪论背景、意义、国内外现状。相关技术介绍Java、SSM、Flask、MySQL、Maven。系统分析可行性分析、功能需求、角色用例图、业务流程。系统设计架构图、技术方案、数据库ER图、核心表结构。功能实现每个模块放一张页面截图和一段核心逻辑描述。系统测试测试环境、功能测试用例、性能测试、结论。总结与展望。重点要在第2章和第4章。第4章的数据库ER图尤其要画清楚预算表、报销表、流水表之间的关系如果逻辑正确老师在评阅时一眼就知道你是真做了。6.2 调试文档怎么整理调试文档的价值在于“别人拿到你的系统后能复现”。我建议做成这样的表格模块现象原因解决方案数据库连接启动报时区错误MySQL 8时区问题jdbc.url加serverTimezone报销单提交预算校验不通过剩余额度不足查看fund_budget表中used/freezeFlask附件解析接口404Flask端口写错修改SSM配置中的Flask地址页面中文乱码JSP显示?号编码不一致统一utf-8数据库用utf8mb4每个问题只需要记录现象、原因、解决步骤不需要长篇大论。但这张表在答辩时几乎就是你的现场答辩提示卡。6.3 讲解和答辩的高频问题如果你是要用这个项目做毕设答辩讲的时候按“项目背景—技术架构—功能演示—亮点—不足”这条主线来。功能演示从登录开始优先演示一个完整闭环建项目→录预算→提经费申请→审批→报销→查报表。被问得最多的问题我提前给答案预算并发超支怎么防止答审批提交时用数据库行锁锁定预算行冻结额度审批通过后再转已使用。审批流为什么这样设计答因为科研经费管理要跨不同角色逐级审核细分状态可以让每个参与者都明确当前进度。Flask和SSM为什么不统一成一个技术栈答SSM处理强事务业务保证数据一致Flask利用Python生态处理附件解析和统计导出提升开发效率两者通过HTTP接口解耦。6.4 避坑清单最后再列一份项目实操中反复遇到的坑都是我用实际时间换来的金额字段一定用BigDecimal和DECIMAL数据库和Java两边都要准确。中文乱码是王炸所有环节统一UTF-8数据库连接串加characterEncodingutf8。文件上传保存路径不要写死D:/部署到Linux后路径不通会非常痛苦。可以存相对路径或用SpringMVC的配置。不要在Java里拼接SQLMyBatis尽量用#{}防止SQL注入。密码别明文存储至少用MD5加盐进阶一点用BCrypt。权限拦截器要放行登录页面、静态资源、验证码请求否则登录页都渲染不出来。我个人在实际操作中的体会是这种经费管理系统最怕的不是功能多而是“账对不上”。只要把预算冻结/释放、报销状态流转、流水审计这三条线理顺系统就已经成功了一半。如果你想再往上提升可以给项目加一个仪表盘展示经费执行率预警或者给科研人员加待办消息提醒。这些扩展在答辩现场演示时会比空谈技术栈更能让老师眼前一亮。希望这篇拆解能帮你把项目跑起来、讲清楚顺便少踩几个我踩过的坑。
返回列表