
先说点实在话。每年毕业季我都会被问到同一个问题“JSP 的项目是不是太过时了还能做毕业设计吗”我的回答一直很明确能做而且只要选题和落地思路清晰依然是个拿得出手的题目。今天这篇就围绕“基于 JSP 的高校财务处理系统的设计与实现”这个题目把从选题角度、数据库设计、后端编码到 IDEA 建项目、打 war 包、部署 Tomcat 的完整链路拆开讲一遍。这篇文章不是给你复述课本上的 Servlet 生命周期而是把我实际指导这类项目时踩过的坑、学生最容易卡住的点、以及怎么把这套系统做得既像毕业设计又能真正跑起来一次性说清楚。这套系统解决的核心问题很简单高校里各个院系、部门的预算申报、日常报销、经费审批、数据统计如果全靠纸质流程和线下跑腿效率低且容易出错。把它做成一个 Web 系统让老师在线填报预算、填报报销单让财务处在线审批、控制预算让管理员统一维护用户和权限这就是系统的全部价值。适合正在做 JSP 毕业设计、或者接手类似传统 Java Web 项目的同学参考也适合想快速回顾 JSP Servlet MySQL 整合开发的老开发。1 项目整体设计与选题角度1.1 为什么这个题目选得聪明先说选题。很多同学在毕业设计选题时会犯两个极端错误要么选一个“理论上很先进”但基本做不出来的题目比如“基于深度学习的财务风险预测系统”最后连数据都凑不齐要么选一个“太基础”的题目比如“学生信息管理系统”内容撑不满设计说明书答辩被问两句就露怯。而这个“高校财务处理系统”卡在了一个很好的中间位置。它的业务逻辑足够清晰角色分明流程完整做出来的功能能讲出完整故事而且技术栈上JSP Servlet MySQL Tomcat正好覆盖 Java Web 课程的全部核心知识点。更关键的是它有一个天然的业务闭环部门老师填报预算和报销单财务处审核报销、管理预算额度管理员维护系统基础数据领导查看统计报表有闭环就有流转有流转代码就有状态变化答辩时就有东西可以讲。1.2 技术选型背后的思考可能有同学想问既然市面上真实项目都在用 Spring Boot Vue 前后端分离为什么毕业设计还在用 JSP这里要分清“真实项目选型”和“毕设选型”是两回事。毕设的核心诉求是在有限时间内用你学过的知识完整实现一个有业务深度的系统并能清晰讲清楚实现原理。JSP 虽然老但它有几个优点恰好适合毕设场景不用额外装 Node.js、不用配跨域JSP 页面直接从后端渲染数据调试链路短环境搭建门槛低。评分老师里很大比例是“看传统 Java Web 更顺手”的老师JSP 项目的实现方式他们一眼就能看懂交流成本低。代码可控性高。框架帮我们做的事越少我们自己能写的东西就越多这对毕业设计的“工作量展示”其实是加分项。当然如果选题自由度高我会建议把项目拆成 Spring Boot 后端 JSP 前端这种组合既有传统 JSP 的影子又避开了 Servlet 原生开发里很多重复劳动。但今天这篇还是按原题目走用经典 Servlet JSP 的原生开发方式讲。1.3 系统角色与功能边界在设计系统时第一步不是写代码而是把“谁用系统、用来干什么”想清楚。我习惯把角色和功能画成一张简单的表这张表既是开发依据也是之后写开题报告、建数据库、排工期的底稿。角色核心诉求对应功能模块部门老师在线申报预算、提交报销、查审批进度预算申报、报销单填写、审批进度查询财务处人员审核报销单、控制部门预算、生成凭证数据报销单审核、预算额度控制、支付登记学院/校级领导查看经费使用情况统计报表、预算执行率图表系统管理员维护用户、部门、基础参数用户管理、部门管理、菜单管理、日志管理这个边界很重要。很多学生一上来就想着“我要做全流程财务系统”结果把差旅费标准、会计凭证、资产折旧这些东西全塞进来最后哪个都没做透。毕业设计不是商业软件把一个闭环做完整远胜过把十个功能做得粗糙。2 数据库设计与数据建模2.1 核心表结构设计整个系统最核心、也是在答辩时最能让老师眼前一亮的部分就是“关系模型”设计得好不好。我在指导项目时要求学生的第一件事就是打开 MySQL先把数据库层想清楚再回过来写 Java 类。这个系统的数据模型我按功能域拆成了这么几组用户与权限域-- 用户表 CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50), dept_id BIGINT, role_id BIGINT, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 角色表 CREATE TABLE sys_role ( id BIGINT PRIMARY KEY AUTO_INCREMENT, role_name VARCHAR(30) NOT NULL, role_code VARCHAR(30) NOT NULL UNIQUE ); -- 部门表 CREATE TABLE sys_dept ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(100) NOT NULL, dept_code VARCHAR(30) UNIQUE );在设计用户表时我故意不把“部门名称”直接冗余进 sys_user 里而是通过 dept_id 外键关联。答辩时老师如果问到“为什么这么设计”可以从“减少数据冗余、部门改名只需改一处”这个角度回答。这个点虽然简单但能明显体现你理解关系型数据库的建模思路。业务数据域-- 部门年度预算表 CREATE TABLE bud_budget ( id BIGINT PRIMARY KEY AUTO_INCREMENT, dept_id BIGINT NOT NULL, budget_year INT NOT NULL, total_amount DECIMAL(12,2) NOT NULL, used_amount DECIMAL(12,2) DEFAULT 0.00, status TINYINT DEFAULT 1, UNIQUE KEY uk_dept_year (dept_id, budget_year) ); -- 报销单主表 CREATE TABLE fin_expense ( id BIGINT PRIMARY KEY AUTO_INCREMENT, expense_no VARCHAR(40) NOT NULL UNIQUE, dept_id BIGINT NOT NULL, apply_user_id BIGINT NOT NULL, expense_name VARCHAR(200), amount DECIMAL(12,2) NOT NULL, expense_type INT COMMENT 1-办公用品 2-差旅 3-耗材 4-其他, status INT DEFAULT 0 COMMENT 0-待审核 1-财务通过 2-驳回 3-已支付, apply_time DATETIME, audit_user_id BIGINT, audit_time DATETIME, audit_comment VARCHAR(500) ); -- 报销明细表 CREATE TABLE fin_expense_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, expense_id BIGINT NOT NULL, item_name VARCHAR(200), quantity INT, unit_price DECIMAL(12,2), amount DECIMAL(12,2) );预算表这里有个小技巧我加了一个唯一键 (dept_id, budget_year),目的是防止同一个部门在同一个年度被重复插入两条预算记录。如果程序里没做这个约束用户连续点两次提交数据库直接拒绝第二笔这对系统的健壮性是非常有效的兜底。2.2 状态字段是业务流转的灵魂在业务表中频繁出现的 status 字段是毕业设计里最值得花心思设计的地方。很多初学者会把“待审核、已通过、已驳回”直接设计成三个表这是典型的错误设计。正确做法是在一个表中用数字枚举状态并辅以审核时间、审核人等冗余字段表达流转轨迹。以 fin_expense 表为例我建议的状态流转是这样0待审核老师提交后1财务通过但还没付款2驳回老师能看到驳回原因可修改重提3已支付财务完成打款这条流转线覆盖了“提交 - 审核 - 支付”的完整业务闭环。在 JSP 页面展示报销单列表时只需要根据 status 的数字映射成中文标签再用不同的 CSS 颜色强调界面逻辑就非常直观了。2.3 数据库设计的几个心得第一钱相关的字段永远用 DECIMAL不要用 FLOAT。FLOAT 在高精度计算时会有精度丢失财务系统里一分钱都不能差DECIMAL(12,2) 可以精确到分。这是代码逻辑之外的一个隐藏考点老师看过细节的话会很加分。第二时间字段统一由数据库生成不要自己拼接字符串。创建时间直接用 DEFAULT CURRENT_TIMESTAMP 就行业务时间比如申请时间在程序里用new Date()传入也行但要保证格式统一。第三一定给核心业务表加唯一标识字段比如报销单的 expense_no。这个单号可以自己写规则生成比如BX 日期 流水号用于线下对账和用户查询。它看起来只是个小字段却是完整业务系统区别于玩具项目的重要标志。3 核心功能模块与代码实现细节3.1 登录认证与 RBAC 权限控制登录功能是所有 Web 项目的门面也是 JSP 项目里最能体现 Servlet 基本功的部分。我的实现思路是提交登录表单到 CheckLoginServletServlet 从数据库比对用户名密码成功后把用户对象放进 session然后根据角色跳转不同首页。这里分享一个能明显提升项目质量的点不要只做“登录成功/失败”二分法而是要结合角色字段做权限控制。RBAC基于角色的访问控制核心思想就一句话不对用户直接授权而是对角色授权再把用户放进角色里。体现在代码上最直接的写法是写一个简单的过滤器。public class AuthFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); String uri req.getRequestURI(); // 放行登录页和静态资源 if (uri.endsWith(login.jsp) || uri.contains(/css/) || uri.contains(/js/) || uri.endsWith(LoginServlet)) { chain.doFilter(request, response); return; } if (session null || session.getAttribute(loginUser) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } // 查询该用户角色可访问的菜单权限这一步略 chain.doFilter(request, response); } }在 web.xml 里配置过滤器映射时要注意过滤范围是 /还是 /pages/** 决定了很多后续调试的难度**。我个人建议只过滤需要登录的页面路径比如 /pages/**这样登录页、静态资源、Servlet 都能少一层拦截减少 404 和死循环问题。3.2 预算申报与剩余额度计算预算模块是高校财务系统里最有区分度的功能。核心逻辑是部门负责人填写年度预算总额系统保存后在该部门的每一笔报销通过时动态扣减剩余额度。我让学生在 bud_budget 表里同时维护了 total_amount 和 used_amount 两个字段used_amount 在报销单状态变为“财务通过”时累加。这个方案的好处是查询报表时不需要每次 SUM 所有报销单效率高而且报表页打开就是秒开。坏处是如果数据出现不一致需要手工重算。考虑到毕设场景效率优先是合理的取舍。关键代码放在一个 Service 类里public synchronized void updateBudgetUsedAmount(Long deptId, BigDecimal auditAmount) { String sql UPDATE bud_budget SET used_amount used_amount ? WHERE dept_id ? AND status 1 AND (total_amount - used_amount) ?; int rows jdbcTemplate.update(sql, auditAmount, deptId, auditAmount); if (rows 0) { throw new BudgetNotEnoughException(部门预算额度不足或已被锁定); } }这里有个 SQL 层面的巧妙点把“额度校验”直接写进 UPDATE 条件的 WHERE 子句里。如果剩余额度不足影响行数就是 0程序就知道要抛异常回滚了。这比先把额度查出来再在 Java 里判断更安全规避了并发情况下两个请求同时读到同一个可用额度的问题。这个细节写进文档里是很有含金量的一段。3.3 报销单申请与明细管理报销单模块是下面要讲的所有模块里代码量最大、JSP 页面最多、也最容易出 BUG 的部分。一个完整的报销单由主表和明细表两部分组成主表保存整体信息明细表保存每一笔具体费用。我建议在 JSP 页面上用动态添加行的方式让用户录明细就是页面上放一个“添加一行”按钮每次往 HTML 表格里 append 一行输入框。这里学生容易踩的坑是提交到 Servlet 端后不知道一次性到底传了多少行参数过来。后端解析动态表格的经典写法// 明细表单项命名格式: itemName_0, itemName_1, quantity_0 ... String[] itemNames request.getParameterValues(itemName); String[] quantities request.getParameterValues(quantity); String[] unitPrices request.getParameterValues(unitPrice); if (itemNames ! null) { for (int i 0; i itemNames.length; i) { if (itemNames[i] null || itemNames[i].trim().isEmpty()) { continue; // 跳过空行 } String itemName itemNames[i]; int quantity Integer.parseInt(quantities[i]); BigDecimal unitPrice new BigDecimal(unitPrices[i]); // 逐条插入 fin_expense_item } }注意这里用 getParameterValues 取回的是一个字符串数组它的顺序和页面 input 的出现顺序一致。千万不能只用 getParameter 取单个值那样只能取到第一行一定是出错点。整个报销过程要开启事务。我的建议是在 Service 层方法上加事务控制先插入主表拿到自增ID再循环插入明细表中间任何一步异常就回滚绝不能让主表和明细表数据对不上。3.4 审批流程与消息提醒设计审批流转是整个系统里最能体现“设计能力”的功能。我的实现思路是财务处人员登录后进入报销审核列表看到所有待审核的单据点击进入详情页可以看到主表、明细表以及申报人、部门信息操作只有两个按钮——通过、驳回。比较进阶一点的做法是引入“多级审批”状态。比如部门领导初审 - 财务处复核 - 校长终审。但我不推荐在毕设里铺得太宽两级审批足够体现流程思想。如果非要做加一个 level 字段保存当前审批层级即可每通过一次 level1直到达到最大层级则变为最终通过状态。驳回的场景下一定要做一件事保存审核意见 audit_comment。学生经常忽略这个字段导致老师驳回后报销人完全不知道为什么被驳回系统体验就很差。而有了这个字段在报销人的 JSP 列表页里就多展示一列“审批备注”整个业务闭环就完整了。3.5 个人中心与信息展示页面热搜词里有个“jsp个人信息展示页面”恰恰是这个项目里容易被忽略但是页面量很大的部分。信息展示页不仅仅是把数据库字段打印出来而是要有合理的信息组织和可交互性。我建议每个人信息页至少包含这些区块个人基础资料卡片姓名、部门、角色、注册时间与我相关的报销单列表我发起的、待我审核的统计小卡片我的总报销金额、最近30天报销次数这里可以顺手用到“饿了么 elment 图标”相关的思路——我说的是借道 Element UI 的视觉语言。虽然 JSP 项目不直接引入 Vue但我们可以复用 Element UI 的图标库和样式风格把页面做得干净一点。你不用去 npm 安装直接 CDN 引入图标字体文件把经典的 el-icon-user、el-icon-document、el-icon-money 这几个图标用在个人信息卡片上就能让页面看起来比默认的 Bootstrap 表格专业很多。link relstylesheet hrefhttps://unpkg.com/element-ui/lib/theme-chalk/index.css不过要提醒一句用外部 CDN 意味着演示环境必须联网。如果答辩时现场网络不稳页面就会很尴尬。保险做法是把自己的静态资源全部下载到本地放如 webapp/static 目录。3.6 报表统计与图表展示报表模块是给领导看的承担着“展示项目完成度上限”的任务。最简单实用的是按部门汇总本年预算执行率年度预算总额、已支出总额、预算执行率已支出除以总预算。如果只想做纯 JSP 展示用 HTML 表格列出汇总数据就够了。想在视觉上加分可以引入 ECharts 的 CDN。ECharts 是纯前端图表库数据用 JSP 页面在服务端拼成 JSON 字符串塞进页面里然后前端 JS 读取后渲染图表。这个组合方式不需要任何 Ajax 知识只需要 JSP 输出 JSON 文本。script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script div idchart stylewidth:100%;height:400px;/div script var myChart echarts.init(document.getElementById(chart)); var data % jsonData %; // JSP 输出的 JSON 字符串 myChart.setOption({ tooltip: {}, xAxis: { data: data.categories }, yAxis: {}, series: [{ type: bar, data: data.values }] }); /script这个“JSP 后端拼 JSON、前端直接解析”的模式值得你写出一个专门的 JSP 页面作为报表页。它的好处是让你绕过原生 Servlet 手写 JSON 输出的麻烦又能直观展示前后端协作的数据流。4 项目实操从 IDEA 建项目到部署上线4.1 IDEA 新建 JSP 项目的标准流程很多同学第一次用 IDEA 建 JSP 项目时都有点懵因为新版 IDEA 已经不太直接提供 Java Web 项目的创建入口了。这里把流程完整走一遍。打开 IntelliJ IDEA选择 File - New - Project。左侧选择 Java右侧要确保 Project SDK 选的是 JDK 8 或 11。不要直接选 Spring Boot那是另一个技术栈。建好 Java 项目后手动给它加上 Web 能力右键项目名 - Add Framework Support - 勾选 Web Application。这时 IDEA 会自动生成 web/WEB-INF 目录和 web.xml。接下来要做的是配置 Tomcat。在 Run/Debug Configurations 里新增 Tomcat Server - Local在 Deployment 标签页里点加号选择 Artifact - 选中项目名:war exploded。这个war exploded 模式是开发时最推荐的它把项目以解压目录的方式跑在 Tomcat 里改 JSP 刷新即生效不用重启非常舒服。4.2 传统 JSP 项目打包 war 的关键操作毕设快收尾时你需要把项目打成war 包丢给答辩环境。IDEA 里两种稳妥的打包方式方式一在项目结构里配置 Artifacts - 点加号 - 选择 Web Application: Archive - 比如命名为finance.war然后 Build - Build Artifacts就会在输出目录生成 war 文件。方式二如果项目已配置 Maven直接在 pom.xml 里设置packagingwar/packaging然后执行mvn clean packagetarget 目录下就会出现 war 包。打包完毕后把这个 war 文件直接丢到 Tomcat 的 webapps 目录下再启动 Tomcat它会自动解压并部署。访问地址类似http://localhost:8080/finance/login.jsp这里最大的坑是上下文路径。如果你的项目访问路径带着 /finance所有 JSP 里的相对路径、Servlet 跳转路径都得考虑这个前缀。所以我强烈建议在项目里任何跳转都用request.getContextPath()拼前缀避免部署后路径满天飞。resp.sendRedirect(req.getContextPath() /pages/dashboard.jsp);4.3 JSP 页面加载完成后刷新一次的前端技巧热搜词里出现“jsp页面让加载完后刷新一次”这其实是个前端技巧。虽然看上去像“偏门需求”但在本项目里非常有用比如报销单提交成功后我们希望页面展示最新数据或者某个列表页在用户按 F5 时会重复提交表单——这个问题的根治方案其实是 Post/Redirect/Get 模式而不是页面刷新。PRG 模式的标准做法是Servlet 在处理完 POST 请求后不是直接 forward 到一个 JSP而是先 redirect 到另一个 GET 请求地址。这样浏览器地址栏变成 GET 地址刷新时不会再次提交表单。这比在 JSP 里塞response.setHeader(refresh, 1)优雅得多也避免了重复插入数据库的风险。如果确实需要实现“页面加载完成后自动刷新一次”可以用 HTML 的 meta refreshmeta http-equivrefresh content0但坦率讲这种写法在毕设项目里基本用不到甚至会造成页面闪烁。我更推荐把精力放在 PRG 模式上这也是我在批改代码时容易发现的“隐藏加分点”。4.4 环境配置要点与常见报错不同机器上的 JDK、Tomcat、MySQL 版本差异会导致整个项目无法运行。我建议统一使用 JDK 8 Tomcat 8.5 / 9 MySQL 5.7。这三组版本是经过大量毕设项目验证的稳定组合兼容性最好。JSP 连接 MySQL 的 JDBC 驱动现在版本很多8.0 以上版本的 driver 名称和连接串跟旧版不一样很容易报 ClassNotFoundException。我用的是 mysql-connector-java 8.0.28 时连接串这样写jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/finance_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456如果用的是 MySQL 5.7还可以用老驱动com.mysql.jdbc.Driver但我建议统一用新版驱动因为新版驱动兼容旧版本数据库反过来就不一定了。5 常见问题与排查技巧实录5.1 JSP 页面中文乱码这是毕设项目里出现频率第一的问题没有之一。乱码的根源只有一个某个环节的字符集不一致。链路上有这三个关键点JSP 文件本身的编码在文件头声明% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%Servlet 读取请求参数时的编码在 doPost 方法最前面写request.setCharacterEncoding(UTF-8);数据库连接串上的编码参数characterEncodingutf8三个地方全设置成 UTF-8 后大部分乱码会消失。如果还乱查一下 MySQL 建库时的字符集是不是 utf8mb4show create database finance_db看一眼即可。5.2 404访问不到 Servlet 和 JSP毕业答辩现场最容易翻车的问题就是 404。原因通常有这几类web.xml 里 Servlet 映射配错了比如把 url-pattern 写成/LoginServlet/多的那个斜杠会导致完全无法访问。正确写法是/LoginServlet。路径少写了项目上下文部署后访问地址是/finance/xxx如果你直接写/xxx就 404 了。Tomcat 版本太新比如 Tomcat 10 的包名从 javax 变成了 jakarta老项目直接跑不了。这个必须在项目一开始就锁定版本。排查 404 的方法很简单打开浏览器开发者工具F12看请求 URL 是什么、服务器返回什么再对照 web.xml 里的映射规则一步步查。5.3 500 异常与空指针JSP 新手最常见的错误是在页面脚本里直接引用不存在的属性比如 session 里的对象还没放进去就取属性于是拿到的对象是 null再点属性就抛 NullPointerException。排查空指针的经验是在从 session / request 取值后先判断是否为 null 再往下执行。而在 Servlet 端我建议在后端代码里打日志System.out.println 也行把关键参数和查询结果的消息打印出来。答辩演示时控制台的日志输出本身就是你调试过程的直接证据。5.4 表单重复提交问题报销单被提交两次数据库里出现两条一模一样的数据这是高并发场景下常见的脏数据问题但在毕设项目中也会因为网速慢、用户双击按钮而出现。除了前面说的 PRG 模式之外前端还可以做两件事提交按钮提交后立刻置灰防止二次点击。在表单里放一个隐藏 token首次渲染时把 token 存到 sessionServlet 处理时检查 token 是否匹配不匹配则拒绝提交。第二种方案代码略多但在项目文档里能体现出你对数据一致性的思考。5.5 五个高频问题的速查表我把上面几个问题整理成一个快速排查表有需要时直接对着查。异常表现核心原因解决方案页面 HTML 源码乱码JSP 编码声明缺失加 pageEncodingUTF-8中文数据在数据库乱码连接串缺字符集参数URL 加 characterEncodingutf8访问 Servlet 404映射或上下文路径错误检查 web.xml使用 getContextPath()ClassNotFoundExceptionJDBC 驱动版本不匹配使用 8.0 驱动 com.mysql.cj.jdbc.Driver数据重复插入重复提交未处理前端禁按钮 PRG 模式6 实操总结与个人体会最后说点我在实际带这类项目时的心里话。很多同学会纠结“JSP 太老了”“别人的项目用了 Spring Cloud”之类的问题但毕业设计的核心评价标准一向不是技术新旧而是你是否完整、正确地实现了一个有业务价值的系统并且能把它讲清楚。我见过太多用所谓“最新技术”拼凑出来的半成品答辩时却连自己的数据表关系都说不明白反而是一个老老实实把报销流程做闭环、能展示预算执行率图表、还能现场打个 war 包部署起来的 JSP 项目让评委老师频频点头。我个人在实操中的体会是这类项目的成败往往不取决于高深的算法而取决于那些最基础的工程素养——数据库字段设计是否合理、状态流转是否完整、字符集是否统一、路径处理是否规范。你在做项目的过程中把这些细节一个不落地处理好顺便多看几遍自己的 SQL 和 Servlet 代码等答辩时那些不是靠背、而是靠动手理解出来的思路自然会成为你最踏实的支撑。