☰
原生Servlet+MySQL财务系统实战:凭证/总账/报表完整闭环
2026/10/1 4:34:42 网站建设 项目流程

简介:本资源是一套基于JavaEE原生Servlet与MySQL开发的企业财务管理系统完整毕业设计项目,面向计算机相关专业本科生及Java Web初学者,解决课程大作业与毕业设计中系统开发、数据库建模与前后端协同实现的典型需求。压缩包共117.66MB,包含可直接运行的Java Web源码、结构清晰的MySQL数据库SQL脚本、逻辑完整的毕业论文(含需求分析、系统设计、核心代码说明与测试结果)、以及配套功能演示与部署讲解视频,覆盖从环境搭建、模块开发到系统测试的全流程。目前已有113人学习下载,内容组织规范,各模块(如用户管理、账务录入、报表统计、权限控制)均提供独立可调试代码段与注释说明,论文格式符合高校基本要求,视频时长充足、操作步骤连贯,便于快速理解业务逻辑与技术实现路径。

1. 这不是又一个“登录注册+增删改查”的Java Web Demo:它是一套能跑通完整财务业务闭环的原生Servlet+MySQL实战系统,专为毕业设计和课程大作业卡点救急而生

你手头正赶着Java EE课设 deadline?导师要求“必须用原生 Servlet + JDBC + MySQL 实现,禁用 Spring Boot、Struts、Hibernate 等框架”?翻遍 GitHub 和 CSDN,全是“用户管理系统”“图书借阅系统”——功能单薄、逻辑松散、连个真实财务场景的影子都没有?别硬撑了。这套4.2javaEE_原生Servlet_MySql企业财务管理系统就是冲着这个痛点来的:它不玩概念,不堆花哨前端,而是把「凭证录入→科目汇总→总账生成→利润表/资产负债表导出」这一整条中小企业真实财务流水线,用最朴素的HttpServlet、JDBC、JSP+JSTL拆解成可调试、可修改、可答辩的代码模块。它包含完整的数据库建模(含会计科目树、凭证分录、辅助核算字段)、事务控制(转账、结账必须原子性)、权限分级(出纳/会计/主管三级操作隔离),甚至预留了报表导出 Excel 的 POI 接口。这不是教学玩具,是能让你在答辩时指着AccountingService.java里那段带@Override protected void doPost(HttpServletRequest req, HttpServletResponse resp)的真实业务逻辑,底气十足地说:“老师,这里我实现了借贷平衡校验,如果金额不平,整个事务 rollback,不会写入脏数据。” —— 对,就是这种能讲清楚“为什么这么写”的项目,才是毕业设计该有的样子。


2. 从零启动:环境搭建与项目结构解剖——为什么选 Tomcat 9 + JDK 8 + MySQL 5.7 而不是更新版本?

这套系统不是为最新技术栈设计的,它的价值恰恰在于“可控”。作为课程作业或毕设,稳定压倒一切。Tomcat 9 兼容 Servlet 4.0 规范,足够支撑所有业务;JDK 8 是高校实验室和旧版 IDE(如 Eclipse Mars)的标配,避免var关键字或模块化带来的兼容问题;MySQL 5.7 提供了JSON类型支持(用于存储凭证摘要)和稳定的GROUP_CONCAT(用于科目汇总),同时避开了 8.0 的严格模式默认开启导致的sql_mode报错玄学。下面带你一步步还原这套环境的最小可行配置。

2.1 JDK 8 安装与验证:别跳过JAVA_HOME的路径空格陷阱

很多同学在 Windows 上装完 JDK,java -version显示正常,但一运行 Servlet 就报ClassNotFoundException: javax.servlet.http.HttpServlet——根源往往在JAVA_HOME。如果你的 JDK 装在C:\Program Files\Java\jdk1.8.0_202,注意Program Files中间的空格会破坏 Tomcat 启动脚本里的路径拼接。正确做法是:

# 在系统环境变量中设置 JAVA_HOME(路径不含空格) JAVA_HOME = C:\Progra~1\Java\jdk1.8.0_202 # 或者更稳妥:重装到无空格路径,例如 D:\jdk8

提示:Progra~1是 Windows 的 8.3 文件名缩写,比手动加引号更可靠。设置后重启命令行,执行echo %JAVA_HOME%确认输出无误,再运行javac -version验证编译器可用。

2.2 Tomcat 9 解压即用配置:关键三步绕过 404 和 403

Tomcat 不是安装程序,是绿色解压包。但新手常卡在“部署后访问 404”或“Manager 页面 403”。核心在于conf/tomcat-users.xml的角色绑定和webapps目录结构:

<!-- conf/tomcat-users.xml 中添加以下内容(注意 <role> 必须在 <user> 之前) --> <role rolename="manager-gui"/> <role rolename="manager-script"/> <user username="admin" password="123456" roles="manager-gui,manager-script"/>

保存后重启 Tomcat,访问http://localhost:8080/manager/html即可看到应用管理页。部署本项目时,不要直接把整个 ZIP 解压到webapps下——webapps只接受 WAR 包或符合 Servlet 规范的目录结构。正确做法是:

  1. 将下载包解压,找到src(Java 源码)、WebContent(JSP/HTML/JS/CSS)、WEB-INF(含web.xml)目录;
  2. 在webapps下新建文件夹finance-system;
  3. 将WebContent内所有内容(含WEB-INF)复制到finance-system目录下;
  4. 启动 Tomcat,访问http://localhost:8080/finance-system/login.jsp。

2.3 MySQL 5.7 初始化:字符集与用户权限必须显式声明

财务系统对中文支持和权限隔离要求极高。MySQL 默认latin1字符集会导致科目名称乱码,root@localhost权限过大则违反最小权限原则。执行以下 SQL 初始化数据库:

-- 创建数据库并指定字符集(关键!) CREATE DATABASE finance_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 创建专用用户(非 root) CREATE USER 'finance_user'@'localhost' IDENTIFIED BY 'Fin@2024'; GRANT SELECT, INSERT, UPDATE, DELETE ON finance_db.* TO 'finance_user'@'localhost'; -- 刷新权限 FLUSH PRIVILEGES;

参数说明:utf8mb4支持 emoji 和四字节 UTF-8 字符(如某些生僻财务符号);finance_user仅拥有 CRUD 权限,杜绝DROP TABLE风险;密码Fin@2024包含大小写字母+数字+特殊字符,满足多数学校数据库安全要求。

2.4 项目结构深度解析:看懂src目录下的三层责任划分

本项目未用 Maven,采用传统src目录结构,但逻辑清晰。打开src文件夹,你会看到三个核心包:

包名核心类示例职责说明
com.finance.servletLoginServlet.java,VoucherAddServlet.java控制器层:接收 HTTP 请求,调用 Service,转发响应。每个 Servlet 对应一个业务动作(如新增凭证),不处理业务规则。
com.finance.serviceAccountingService.java,ReportService.java服务层:封装核心业务逻辑。例如AccountingService.addVoucher()内部完成:① 借贷金额校验 ② 科目余额更新 ③ 凭证号自增 ④ 事务提交/回滚。这是答辩时老师最爱问的“关键逻辑”所在。
com.finance.daoVoucherDao.java,SubjectDao.java数据访问层:纯 JDBC 操作。VoucherDao.insert()方法里能看到PreparedStatement的?占位符、executeUpdate()返回值判断、ResultSet手动映射——这才是原生 Servlet 教学要求的“底层实现”。

注意:WEB-INF/web.xml是整个系统的路由中枢。打开它,你会看到<servlet>标签定义了LoginServlet类,<servlet-mapping>将/loginURL 映射到该类。修改 URL 路径必须同步改这两处,否则 404。


3. 数据库落地:从finance.sql到可运行的财务账套——科目树、凭证分录、辅助核算字段的设计深意

finance.sql不是简单的CREATE TABLE拼接,它是一套经过会计实务验证的模型。直接执行source finance.sql很可能失败——因为缺少前置依赖或字符集冲突。我们必须理解每张表存在的理由,才能修好它。

3.1 科目表t_subject:为什么用parent_id而不用level字段?

财务科目是典型的树形结构(如:资产类 → 流动资产 → 货币资金 → 库存现金)。常见错误是用level字段(1=一级,2=二级)表示层级,但这会导致插入新科目时需手动计算level,且无法动态查询某科目下所有子科目。本项目采用parent_id(自引用外键):

CREATE TABLE t_subject ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL COMMENT '科目编码,如 1001', name VARCHAR(100) NOT NULL COMMENT '科目名称', parent_id INT DEFAULT NULL COMMENT '父科目ID,NULL 表示一级科目', is_leaf TINYINT(1) DEFAULT 0 COMMENT '是否末级科目(1=是,0=否)', FOREIGN KEY (parent_id) REFERENCES t_subject(id) );

实操技巧:插入科目时,先插一级科目(parent_id为NULL),再插二级(parent_id指向一级id)。is_leaf字段由程序在插入时根据parent_id是否为空自动设置,禁止人工修改——它决定了该科目能否录入凭证(只有末级科目才能记账)。

3.2 凭证表t_voucher与分录表t_voucher_entry:一对多关系如何保证借贷平衡?

财务核心是“有借必有贷,借贷必相等”。本系统将凭证主信息(日期、摘要、制单人)存在t_voucher,每笔分录(科目、方向、金额)存在t_voucher_entry,通过voucher_id关联。关键校验逻辑在AccountingService.addVoucher()中:

// 伪代码:实际在 AccountingService.java 第 127 行 public boolean addVoucher(Voucher voucher) { // 1. 开启事务 conn.setAutoCommit(false); try { // 2. 插入凭证主表 String sql = "INSERT INTO t_voucher (...) VALUES (...)"; // 3. 遍历分录列表,插入明细表 for (VoucherEntry entry : voucher.getEntries()) { insertEntry(entry); // 每次 insert 都用同一个 conn } // 4. 计算总借方、总贷方 BigDecimal debitSum = voucher.getDebitSum(); BigDecimal creditSum = voucher.getCreditSum(); if (!debitSum.equals(creditSum)) { throw new RuntimeException("凭证借贷不平衡!"); } // 5. 提交事务 conn.commit(); return true; } catch (Exception e) { conn.rollback(); // 重点!任何异常都回滚 return false; } }

参数说明:BigDecimal用于金额计算,避免float/double的精度丢失;conn.rollback()是财务系统的生命线——哪怕只有一条分录插入失败,整个凭证都不能生效。

3.3 辅助核算表t_auxiliary:为什么设计aux_type和aux_code两个字段?

中小企业常需按部门、项目、客户核算费用。若为每个辅助项建一张表(t_department,t_project),会导致t_voucher_entry表字段爆炸。本系统采用泛型设计:

CREATE TABLE t_auxiliary ( id INT PRIMARY KEY AUTO_INCREMENT, aux_type VARCHAR(20) NOT NULL COMMENT '类型:DEPT/PROJ/CUST', aux_code VARCHAR(50) NOT NULL COMMENT '编码,如 DEPT-001', aux_name VARCHAR(100) NOT NULL COMMENT '名称,如 销售部' ); -- 分录表中增加辅助核算字段 ALTER TABLE t_voucher_entry ADD COLUMN aux_id INT DEFAULT NULL; ALTER TABLE t_voucher_entry ADD FOREIGN KEY (aux_id) REFERENCES t_auxiliary(id);

好处:新增辅助核算类型(如“供应商”)只需在t_auxiliary插入aux_type='SUPP'记录,无需改表结构。aux_code保证跨类型唯一(DEPT-001和PROJ-001可共存)。

3.4 报表视图v_balance_sheet:用UNION ALL拼接资产与负债,而非硬编码 SQL

资产负债表需合并“货币资金”“应收账款”等资产类科目余额,与“短期借款”“应付账款”等负债类科目余额。若在 Java 代码里写死 SQL,维护成本极高。本项目创建数据库视图:

CREATE VIEW v_balance_sheet AS -- 资产类(借方余额) SELECT 'ASSET' AS type, s.name AS subject_name, COALESCE(SUM(e.amount), 0) AS balance FROM t_subject s LEFT JOIN t_voucher_entry e ON s.id = e.subject_id AND e.direction = 'DEBIT' WHERE s.code LIKE '1%' OR s.code LIKE '2%' -- 1xx/2xx 为资产类 GROUP BY s.id, s.name UNION ALL -- 负债类(贷方余额) SELECT 'LIABILITY' AS type, s.name, COALESCE(SUM(e.amount), 0) AS balance FROM t_subject s LEFT JOIN t_voucher_entry e ON s.id = e.subject_id AND e.direction = 'CREDIT' WHERE s.code LIKE '2%' OR s.code LIKE '4%' -- 2xx/4xx 为负债类 GROUP BY s.id, s.name;

优势:Java 层只需SELECT * FROM v_balance_sheet,逻辑完全下推到数据库。COALESCE(SUM(), 0)防止NULL导致报表空白——这是财务系统不容忽视的细节。


4. 避坑指南:那些让答辩前夜崩溃的 5 个高频故障与血泪修复方案

这套系统在真实环境中跑起来,远比想象中“脆弱”。我帮 17 个同学部署过,90% 的问题集中在以下五类。它们不是代码 bug,而是环境、配置、认知偏差导致的“玄学失败”。列在这里,帮你省下通宵 debug 的时间。

4.1 现象:登录成功后跳转index.jsp,页面显示HTTP Status 404 – Not Found

原因:index.jsp中<jsp:include page="header.jsp"/>失败,因header.jsp路径错误或web.xml中welcome-file-list未配置。
解决:

  1. 检查WebContent目录下是否存在header.jsp(应在根目录,非WEB-INF下);
  2. 打开WEB-INF/web.xml,确认<welcome-file-list>包含<welcome-file>index.jsp</welcome-file>;
  3. 若index.jsp用request.getRequestDispatcher("xxx").forward()转发,确保目标路径以/开头(如/jsp/main.jsp),否则相对路径解析失败。

4.2 现象:新增凭证时提示java.sql.SQLException: Column count doesn't match value count at row 1

原因:t_voucher_entry表结构变更(如新增字段),但VoucherEntryDao.insert()的 SQL 语句未同步更新,VALUES子句的?数量与字段数不匹配。
解决:

  1. 对照finance.sql中CREATE TABLE t_voucher_entry的字段顺序;
  2. 找到VoucherEntryDao.java的insert()方法,检查String sql = "INSERT INTO t_voucher_entry (...) VALUES (?, ?, ?...)";
  3. 关键:VALUES中的?个数必须等于(...)中的字段数,且顺序严格一致(如subject_id, direction, amount, aux_id对应?,?,?,?)。

4.3 现象:MySQL 连接报错Access denied for user 'finance_user'@'localhost',但用户名密码确认无误

原因:MySQL 5.7 默认认证插件为mysql_native_password,而某些 JDBC 驱动(如 8.0+)尝试用caching_sha2_password连接。
解决:

  1. 登录 MySQL:mysql -u root -p;
  2. 执行:ALTER USER 'finance_user'@'localhost' IDENTIFIED WITH mysql_native_password BY 'Fin@2024';;
  3. 刷新:FLUSH PRIVILEGES;;
  4. 在db.properties中确认jdbc.driver=com.mysql.jdbc.Driver(非com.mysql.cj.jdbc.Driver)。

4.4 现象:科目下拉框显示null或乱码,如??

原因:JSP 页面未声明字符集,或web.xml中filter编码过滤器未生效。
解决:

  1. 在login.jsp、add_voucher.jsp等所有 JSP 顶部添加:<%@ page contentType="text/html;charset=UTF-8" %>;
  2. 检查WEB-INF/web.xml中是否有<filter>配置CharacterEncodingFilter,且<filter-mapping>覆盖/*;
  3. 若使用 Tomcat 9,还需在conf/server.xml的<Connector>标签中添加URIEncoding="UTF-8"属性。

4.5 现象:导出 Excel 报表为空白,或只有一行标题

原因:ReportService.exportToExcel()中HSSFWorkbook(对应.xls)与XSSFWorkbook(对应.xlsx)混用,或response.setContentType()设置错误。
解决:

  1. 查看exportToExcel()方法,确认Workbook wb = new HSSFWorkbook();(旧版)或new XSSFWorkbook()(新版);
  2. 若用HSSFWorkbook,response.setContentType("application/vnd.ms-excel");;若用XSSFWorkbook,response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");;
  3. 致命细节:response.setHeader("Content-Disposition", "attachment; filename=balance.xls");中的filename必须与Workbook类型匹配(.xls对应HSSF,.xlsx对应XSSF)。

5. 业务逻辑加固:在AccountingService中植入借贷平衡校验与期末结账锁表机制

仅仅跑通 CRUD 远不够毕业设计的深度。真正的加分项,是让系统具备财务软件应有的“业务约束力”。本章教你如何在现有AccountingService.java基础上,安全地加入两项关键能力:实时借贷平衡校验(防止人为失误)、期末结账锁表(模拟真实月结流程)。这些改动不破坏原有结构,且每一步都有可验证效果。

5.1 借贷平衡校验:从“事后报错”升级为“事前拦截”

当前addVoucher()是在所有分录插入后才校验总金额。但若凭证有 10 笔分录,第 9 笔插入成功、第 10 笔因网络中断失败,rollback()会回滚全部,用户体验差。更优方案是在 Servlet 层就做前端校验,并在 Service 层做二次校验:

第一步:在add_voucher.jsp添加 JavaScript 实时计算

<!-- 在凭证分录表格下方添加 --> <div id="balance-check"> <strong>借方合计:</strong><span id="debit-total">0.00</span> &nbsp;&nbsp; <strong>贷方合计:</strong><span id="credit-total">0.00</span> &nbsp;&nbsp; <span id="balance-status" style="color:red;">不平衡!</span> </div> <script> function updateBalance() { let debit = 0, credit = 0; // 遍历所有分录行 document.querySelectorAll('.entry-row').forEach(row => { const dir = row.querySelector('select[name="direction"]').value; const amt = parseFloat(row.querySelector('input[name="amount"]').value) || 0; if (dir === 'DEBIT') debit += amt; else credit += amt; }); document.getElementById('debit-total').textContent = debit.toFixed(2); document.getElementById('credit-total').textContent = credit.toFixed(2); if (Math.abs(debit - credit) < 0.01) { // 允许浮点误差 document.getElementById('balance-status').textContent = '平衡 ✓'; document.getElementById('balance-status').style.color = 'green'; } else { document.getElementById('balance-status').textContent = '不平衡!'; document.getElementById('balance-status').style.color = 'red'; } } // 绑定到金额输入框的 change 事件 document.addEventListener('change', function(e) { if (e.target.name === 'amount' || e.target.name === 'direction') { updateBalance(); } }); </script>

效果:用户在填写分录时,页面实时显示借贷是否平衡。未平衡时,“提交”按钮被禁用(需在按钮上加disabled属性逻辑),从源头杜绝无效凭证。

第二步:在AccountingService.addVoucher()中强化校验

// 修改前:只在校验总和 if (!debitSum.equals(creditSum)) { ... } // 修改后:增加明细级校验(防 SQL 注入篡改) for (VoucherEntry entry : voucher.getEntries()) { if (entry.getAmount().compareTo(BigDecimal.ZERO) <= 0) { throw new IllegalArgumentException("分录金额必须大于 0"); } if (!"DEBIT".equals(entry.getDirection()) && !"CREDIT".equals(entry.getDirection())) { throw new IllegalArgumentException("方向只能是 DEBIT 或 CREDIT"); } } // 再校验总和(保留原有逻辑) if (debitSum.subtract(creditSum).abs().compareTo(new BigDecimal("0.01")) > 0) { throw new RuntimeException("凭证借贷不平衡,差额:" + debitSum.subtract(creditSum)); }

参数说明:subtract().abs()计算绝对差值,compareTo(new BigDecimal("0.01")) > 0表示差额超过 1 分钱即报错——这是财务系统对精度的敬畏。

5.2 期末结账锁表:用数据库行锁模拟“本月已结账”状态

真实财务软件中,结账后当月凭证不可修改。本系统通过t_period表记录结账状态,并在关键操作(如新增凭证、修改凭证)前检查:

第一步:扩展t_period表,增加is_closed字段

ALTER TABLE t_period ADD COLUMN is_closed TINYINT(1) DEFAULT 0 COMMENT '是否已结账(0=未结,1=已结)'; -- 初始化当前期间(假设为 2024-06) INSERT INTO t_period (year_month, is_closed) VALUES ('2024-06', 0);

第二步:在AccountingService中添加结账检查方法

// 新增方法 public boolean isCurrentPeriodClosed() throws SQLException { String sql = "SELECT is_closed FROM t_period WHERE year_month = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, getCurrentYearMonth()); // 获取当前年月,如 "2024-06" ResultSet rs = ps.executeQuery(); if (rs.next()) { return rs.getInt("is_closed") == 1; } return false; // 期间不存在,默认未结账 } // 在 addVoucher() 开头加入 if (isCurrentPeriodClosed()) { throw new RuntimeException("当前期间已结账,禁止新增凭证!"); }

第三步:实现结账 Servlet(ClosePeriodServlet.java)

protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { AccountingService service = new AccountingService(); // 1. 检查是否存在未审核凭证 if (service.hasUnapprovedVouchers()) { req.setAttribute("error", "存在未审核凭证,不能结账!"); req.getRequestDispatcher("/close_period.jsp").forward(req, resp); return; } // 2. 更新期间状态 service.closeCurrentPeriod(); req.setAttribute("message", "结账成功!"); } catch (Exception e) { req.setAttribute("error", "结账失败:" + e.getMessage()); } req.getRequestDispatcher("/close_period.jsp").forward(req, resp); }

关键逻辑:closeCurrentPeriod()方法内执行UPDATE t_period SET is_closed = 1 WHERE year_month = ?,并确保该操作在事务中完成。这样,一旦结账,所有后续凭证操作都会被拦截。

5.3 验证你的加固是否生效:三步快速测试法

不要等到答辩才验证。现在就用这三步确认你的修改真正起效:

  1. 前端校验测试:打开add_voucher.jsp,随意填一笔分录(借方 100),再填一笔(贷方 99),观察页面balance-status是否显示“不平衡!”且提交按钮禁用;
  2. 后端校验测试:故意在AccountingService.addVoucher()中抛出RuntimeException("测试异常"),提交凭证,确认页面跳转到error.jsp并显示该消息;
  3. 结账锁表测试:访问http://localhost:8080/finance-system/closePeriod(假设你已配置该 Servlet 映射),点击结账,再尝试新增凭证——应看到“当前期间已结账”提示。

从那以后我每次给学生改毕设,都强制他们走一遍这三步验证。不是为了炫技,而是因为财务系统的每一个“不允许”,背后都是真金白银的审计红线。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询