☰
原生Servlet+MySQL企业财务管理系统:从课设到实战的完整实现
2026/10/4 9:48:18 网站建设 项目流程

简介:本资源为基于JavaEE原生Servlet与MySQL的企业财务管理系统完整毕业设计资料包,面向计算机相关专业需要完成毕业设计或课程大作业的学生。内容涵盖系统源码、数据库SQL脚本、论文文档及配套讲解视频,可帮助读者从零搭建一套可运行的财务管理系统,理解Servlet请求处理、JDBC数据库操作与分层架构设计。压缩包为zip格式,整体约117.66MB,包含源码、SQL脚本、论文与视频等多种文件类型,分别用于项目运行、数据库建表、文档撰写参考与操作演示。目前已有114人学习下载,适合需要完整项目案例、论文模板与实操演示的读者参考,便于快速掌握企业级Web应用的开发流程与实现思路。

1. 从一份 JavaEE 课设压缩包说起:原生 Servlet 财务系统到底能跑出什么

很多人第一次看到「4.2javaEE_原生Servlet_MySql企业财务管理系统设计与实现」这类标题,第一反应是课设作业,第二反应是「这玩意儿能学到东西吗」。我当年也这么想,直到接手一个真实的小微企业记账需求,才发现原生 Servlet 这套东西反而是最稳的底座——没有 Spring 的自动装配黑匣子,没有 MyBatis 的映射玄学,请求从HttpServletRequest进来,SQL 从PreparedStatement出去,中间每一层你都能看见。企业财务管理系统这个场景,核心诉求就三件事:凭证录入不能丢、科目余额要对得上、报表查询要快。原生 Servlet + MySQL 的组合,恰好能把这三件事用最少的抽象层做扎实。这篇文章面向两类人:一是要交课设但不想只交个能跑的壳子的学生,二是想用最小依赖搭一套内部记账工具的一线开发者。下面从环境、表结构、Servlet 分层、事务处理一路讲到避坑和进阶,源码和 SQL 的落地路径都会给到。

2. 环境与数据库先落地:JDK、Tomcat、MySQL 8 的最小可用组合

2.1 为什么选原生 Servlet 而不是上框架

企业财务管理系统这类项目,业务逻辑其实不复杂:科目表、凭证主表、凭证分录表、用户表、日志表,五张表能撑起 80% 的功能。上 Spring Boot 当然快,但课设场景下有个现实问题——答辩老师会问你「IoC 容器怎么实现的」「AOP 代理用的 JDK 还是 CGLIB」,你答不上来就是减分项。原生 Servlet 的好处是每一层都暴露在你面前:web.xml里配servlet-mapping,doPost里手动setCharacterEncoding,JDBC 连接自己管。这不是倒退,是把「框架帮你做的事」摊开看一遍。我一般建议先用原生 Servlet 把 CRUD 和事务跑通,再去对比框架帮你省了什么,这个认知差比直接学框架值钱得多。

选型上还有一点:财务系统对数据一致性要求高,MySQL 的 InnoDB 引擎支持事务和行级锁,配合 Servlet 层的手动commit/rollback,能精确控制到「一张凭证的借贷分录要么全写入要么全回滚」。框架的声明式事务虽然方便,但排查问题时你往往不知道事务边界到底在哪,原生写法反而让边界清清楚楚。

2.2 MySQL 8 安装与建库建表

MySQL 安装这块,Windows 下推荐用官方 installer,Linux 下用 rpm 或 apt 都行,注意 8.0 之后默认认证插件是caching_sha2_password,老版本 JDBC 驱动连不上,要么升级驱动到 8.x,要么改回mysql_native_password。建库时字符集统一用utf8mb4,财务系统里科目名称可能有生僻字,utf8三字节存不下。

-- 创建数据库,字符集和排序规则显式指定 CREATE DATABASE finance_db DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE finance_db; -- 科目表:财务系统的骨架 CREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) NOT NULL UNIQUE COMMENT '科目编码,如1001', name VARCHAR(64) NOT NULL COMMENT '科目名称', type TINYINT NOT NULL COMMENT '1资产 2负债 3权益 4成本 5损益', parent_id INT DEFAULT 0 COMMENT '父科目ID,0为一级', balance DECIMAL(18,2) DEFAULT 0.00 COMMENT '当前余额', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 凭证主表:一张凭证一条记录 CREATE TABLE voucher ( id INT PRIMARY KEY AUTO_INCREMENT, voucher_no VARCHAR(32) NOT NULL UNIQUE COMMENT '凭证号,如记-2024-001', voucher_date DATE NOT NULL, maker_id INT NOT NULL COMMENT '制单人', status TINYINT DEFAULT 0 COMMENT '0草稿 1已审核 2已过账', remark VARCHAR(255), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 凭证分录表:借贷方明细 CREATE TABLE voucher_entry ( id INT PRIMARY KEY AUTO_INCREMENT, voucher_id INT NOT NULL, account_id INT NOT NULL, direction TINYINT NOT NULL COMMENT '1借 2贷', amount DECIMAL(18,2) NOT NULL, summary VARCHAR(255), FOREIGN KEY (voucher_id) REFERENCES voucher(id) ON DELETE CASCADE, FOREIGN KEY (account_id) REFERENCES account(id) ) ENGINE=InnoDB;

建表逻辑说明:account表用parent_id做自关联实现科目树,balance字段冗余存储当前余额,避免每次查报表都全表 SUM。voucher和voucher_entry是一对多,外键ON DELETE CASCADE保证删凭证时分录自动清理。金额字段统一用DECIMAL(18,2),绝不能用FLOAT,浮点误差在财务场景是致命的。参数上,code加唯一索引防止科目编码重复,voucher_no同理。索引方面,voucher_date和account_id建议加普通索引,报表按日期和科目查询时能走索引。

2.3 JDBC 连接与连接池的最小配置

原生 Servlet 里直接DriverManager.getConnection每次新建连接,并发一上来就崩。常见做法是引入 Druid 或 HikariCP,但课设场景下用 Tomcat 自带的DataSource也够。在META-INF/context.xml里配:

<Context> <Resource name="jdbc/financeDB" auth="Container" type="javax.sql.DataSource" maxTotal="20" maxIdle="10" maxWaitMillis="10000" username="root" password="your_password" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/finance_db?useUnicode=true&amp;characterEncoding=utf8mb4&amp;serverTimezone=Asia/Shanghai&amp;useSSL=false"/> </Context>

maxTotal控制最大连接数,财务系统并发不高,20 足够;maxWaitMillis是获取连接的超时时间,设 10 秒避免请求堆积。serverTimezone必须显式指定,否则 MySQL 8 会报时区错误。useSSL=false在本地开发没问题,生产环境建议开启并配证书,但课设阶段先关掉省事。

3. Servlet 分层与凭证录入:从请求到落库的完整链路

3.1 三层结构怎么切:Controller、Service、DAO

原生 Servlet 项目最容易写成「一个 Servlet 干所有事」,几百行doPost里又查库又算余额又拼 HTML。正确的切法是三层:Servlet 只负责收参数、调 Service、转发视图;Service 管业务规则和事务;DAO 只管 SQL。以凭证录入为例,VoucherServlet收到表单后,把参数打包成VoucherDTO,传给VoucherService.saveVoucher(),Service 里开事务,先插主表拿到自增 ID,再批量插分录,最后更新科目余额,任何一步失败就回滚。

这个分层的好处是测试方便:DAO 可以单独用 main 方法跑,Service 可以脱离 Tomcat 测,Servlet 层薄到几乎不用测。财务系统的核心逻辑在 Service,比如「借贷必须平衡」「已过账凭证不能修改」,这些规则集中在一处,改起来不会漏。

3.2 凭证录入的 Servlet 实现

@WebServlet("/voucher/save") public class VoucherServlet extends HttpServlet { private VoucherService voucherService = new VoucherService(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 统一编码,必须在 getParameter 之前调用 req.setCharacterEncoding("UTF-8"); resp.setContentType("application/json;charset=UTF-8"); try { VoucherDTO dto = new VoucherDTO(); dto.setVoucherNo(req.getParameter("voucherNo")); dto.setVoucherDate(Date.valueOf(req.getParameter("voucherDate"))); dto.setMakerId(Integer.parseInt(req.getParameter("makerId"))); dto.setRemark(req.getParameter("remark")); // 分录是多行,前端用 entryAccountId[] 数组提交 String[] accountIds = req.getParameterValues("entryAccountId"); String[] directions = req.getParameterValues("entryDirection"); String[] amounts = req.getParameterValues("entryAmount"); String[] summaries = req.getParameterValues("entrySummary"); List<VoucherEntryDTO> entries = new ArrayList<>(); for (int i = 0; i < accountIds.length; i++) { VoucherEntryDTO e = new VoucherEntryDTO(); e.setAccountId(Integer.parseInt(accountIds[i])); e.setDirection(Integer.parseInt(directions[i])); e.setAmount(new BigDecimal(amounts[i])); e.setSummary(summaries[i]); entries.add(e); } dto.setEntries(entries); voucherService.saveVoucher(dto); resp.getWriter().write("{\"code\":0,\"msg\":\"保存成功\"}"); } catch (Exception e) { resp.setStatus(500); resp.getWriter().write("{\"code\":1,\"msg\":\"" + e.getMessage() + "\"}"); } } }

逻辑说明:setCharacterEncoding必须在第一次getParameter前调用,否则中文乱码。分录用数组参数接收,前端表单里同名 input 用name="entryAccountId"即可。金额用BigDecimal构造,避免Double.parseDouble的精度问题。异常统一捕获后返回 JSON,前端根据code判断。参数上,voucherNo建议前端生成或后端按日期+序列生成,不要用数据库自增做主凭证号,财务上凭证号必须连续可读。

3.3 Service 层的事务控制

public class VoucherService { public void saveVoucher(VoucherDTO dto) throws SQLException { // 借贷平衡校验,这是财务系统的铁律 BigDecimal debitSum = BigDecimal.ZERO; BigDecimal creditSum = BigDecimal.ZERO; for (VoucherEntryDTO e : dto.getEntries()) { if (e.getDirection() == 1) debitSum = debitSum.add(e.getAmount()); else creditSum = creditSum.add(e.getAmount()); } if (debitSum.compareTo(creditSum) != 0) { throw new IllegalArgumentException("借贷不平衡:借" + debitSum + " 贷" + creditSum); } Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 int voucherId = VoucherDAO.insertVoucher(conn, dto); for (VoucherEntryDTO e : dto.getEntries()) { VoucherDAO.insertEntry(conn, voucherId, e); // 更新科目余额:借方增加资产,贷方减少 AccountDAO.updateBalance(conn, e.getAccountId(), e.getDirection() == 1 ? e.getAmount() : e.getAmount().negate()); } conn.commit(); } catch (Exception ex) { if (conn != null) conn.rollback(); throw ex; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } } }

事务控制的关键点:setAutoCommit(false)之后所有 SQL 在同一个连接上执行,commit才真正落库。DAO 方法都接收Connection参数,保证用的是同一个连接,这是原生 JDBC 事务的标准写法。余额更新用negate()处理方向,资产类科目借方增加、贷方减少,负债类相反,实际项目里应该根据科目type判断方向,这里简化处理。注意finally里恢复autoCommit再关闭连接,避免连接池回收后状态污染。

4. 报表查询与性能:科目余额表和凭证列表怎么查才不慢

4.1 科目余额表的实时计算与冗余存储

财务系统最常被点开的页面是科目余额表,展示每个科目的期初、本期借贷、期末余额。如果每次都从voucher_entry全表 SUM,数据量上万条后查询会明显变慢。常见做法是双轨:account.balance存当前余额用于快速展示,同时保留voucher_entry明细用于审计和对账。每次凭证过账时更新account.balance,报表页直接读余额字段,需要明细时再查分录。

但这里有个坑:如果凭证被反审核或删除,余额必须回滚。所以voucher表的status字段很关键,只有status=2(已过账)的凭证才影响余额。删除或反审核时,Service 里要反向更新余额。这个逻辑容易漏,漏了就是账对不上,血泪经验。

4.2 分页查询与索引优化

凭证列表页动辄几千条,必须分页。MySQL 用LIMIT offset, size,但offset很大时性能急剧下降,比如LIMIT 10000, 20会扫描前 10000 行。优化方式是记住上一页最后一条的 ID,用WHERE id > lastId LIMIT 20,这叫游标分页。财务系统按日期倒序排列,可以建(voucher_date, id)联合索引。

-- 传统分页,offset 大时慢 SELECT * FROM voucher ORDER BY voucher_date DESC LIMIT 10000, 20; -- 游标分页,快且稳定 SELECT * FROM voucher WHERE voucher_date <= '2024-06-01' AND id < 5000 ORDER BY voucher_date DESC, id DESC LIMIT 20;

索引方面,voucher_date单独索引在范围查询时有用,但和id组成联合索引后,排序和过滤能同时走索引。voucher_entry表的voucher_id外键自动建索引,account_id建议手动加,按科目查明细时用得上。注意不要过度索引,财务系统写入不频繁,但每多一个索引就多一份维护成本。

4.3 报表 SQL 的写法与常见错误

科目余额表的核心 SQL 是按科目分组汇总借贷:

SELECT a.code, a.name, a.balance AS current_balance, COALESCE(SUM(CASE WHEN e.direction = 1 THEN e.amount ELSE 0 END), 0) AS period_debit, COALESCE(SUM(CASE WHEN e.direction = 2 THEN e.amount ELSE 0 END), 0) AS period_credit FROM account a LEFT JOIN voucher_entry e ON a.id = e.account_id LEFT JOIN voucher v ON e.voucher_id = v.id AND v.status = 2 WHERE v.voucher_date BETWEEN '2024-01-01' AND '2024-06-30' GROUP BY a.id, a.code, a.name, a.balance ORDER BY a.code;

这里用LEFT JOIN保证没有发生额的科目也显示。COALESCE处理 NULL 为 0。v.status = 2放在 JOIN 条件里而不是 WHERE,否则会把没有过账凭证的科目过滤掉。常见错误是把状态条件写在 WHERE,导致余额表缺科目,这个坑我踩过不止一次。

5. 避坑与排查:原生 Servlet + MySQL 财务系统的五个高频翻车点

5.1 中文乱码:现象是科目名存进去变问号

现象:前端提交「银行存款」,数据库里存成????。原因:Tomcat 8 之后GET请求默认 URI 编码是 UTF-8,但POST请求体编码取决于request.setCharacterEncoding,不设就是 ISO-8859-1。解决:在doPost第一行调req.setCharacterEncoding("UTF-8"),同时数据库连接 URL 加characterEncoding=utf8mb4,建库建表也用utf8mb4。三处缺一不可,只改一处照样乱码。

5.2 事务不回滚:现象是凭证主表插入了但分录没进去

现象:保存凭证时报错,但voucher表多了一条脏数据。原因:DAO 方法内部各自getConnection,Service 的conn.rollback()回滚的不是同一个连接。解决:所有 DAO 方法签名接收Connection参数,由 Service 统一创建和关闭。这是原生 JDBC 事务最容易翻车的地方,框架帮你管连接时你感觉不到,自己写就必须严格传参。

5.3 余额对不上:现象是科目余额和分录汇总差几分钱

现象:account.balance和SUM(voucher_entry.amount)对不上,差 0.01。原因:金额用了DOUBLE或FLOAT,浮点累加误差。解决:所有金额字段用DECIMAL(18,2),Java 侧用BigDecimal,且BigDecimal比较用compareTo不用equals(equals会比较精度)。另外余额更新时用UPDATE account SET balance = balance + ?让数据库做加法,不要查出来在 Java 里加再写回,并发下会丢更新。

5.4 连接泄漏:现象是跑一会儿就报 Too many connections

现象:系统运行一段时间后所有请求卡死,MySQL 报连接数超限。原因:Connection、PreparedStatement、ResultSet没有在finally里关闭,或者异常路径漏关。解决:用 try-with-resources 语法,或者严格在finally里按ResultSet → Statement → Connection顺序关闭。连接池配了maxTotal=20,泄漏 20 次就崩。排查时可以查SHOW PROCESSLIST看哪些连接处于Sleep状态。

5.5 凭证号重复:现象是并发保存时唯一索引冲突

现象:两个用户同时保存,报Duplicate entry for key 'voucher_no'。原因:凭证号用「查最大值+1」生成,并发下两个请求查到同一个最大值。解决:凭证号生成要么用数据库序列(MySQL 没有原生序列,可以用单独的表加行锁),要么在voucher_no上加唯一索引后捕获异常重试。更稳的做法是用日期+用户ID+时间戳生成,但财务要求连续编号,所以常见方案是建一张seq表,UPDATE seq SET val = val + 1 WHERE name = 'voucher'配合事务行锁。

6. 进阶技巧:用存储过程做月末结转和余额校验

月末结转是财务系统里逻辑最绕的部分:把损益类科目余额结转到本年利润,然后清零损益类科目。这个操作涉及多表多行,放在 Java 里写一长串 SQL 容易出错,用 MySQL 存储过程反而清晰。下面这个存储过程做两件事:校验所有凭证借贷平衡,然后执行结转。

DELIMITER // CREATE PROCEDURE month_end_close(IN p_year INT, IN p_month INT) BEGIN DECLARE v_unbalanced INT DEFAULT 0; DECLARE v_profit DECIMAL(18,2) DEFAULT 0; -- 校验:找出借贷不平衡的已过账凭证 SELECT COUNT(*) INTO v_unbalanced FROM ( SELECT v.id, SUM(CASE WHEN e.direction = 1 THEN e.amount ELSE 0 END) AS debit, SUM(CASE WHEN e.direction = 2 THEN e.amount ELSE 0 END) AS credit FROM voucher v JOIN voucher_entry e ON v.id = e.voucher_id WHERE v.status = 2 AND YEAR(v.voucher_date) = p_year AND MONTH(v.voucher_date) = p_month GROUP BY v.id HAVING debit <> credit ) t; IF v_unbalanced > 0 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '存在借贷不平衡的凭证,无法结转'; END IF; -- 计算损益类科目(type=5)净额 SELECT COALESCE(SUM(CASE WHEN e.direction = 2 THEN e.amount ELSE -e.amount END), 0) INTO v_profit FROM voucher_entry e JOIN account a ON e.account_id = a.id JOIN voucher v ON e.voucher_id = v.id WHERE a.type = 5 AND v.status = 2 AND YEAR(v.voucher_date) = p_year AND MONTH(v.voucher_date) = p_month; -- 结转分录:损益类清零,本年利润增加 INSERT INTO voucher(voucher_no, voucher_date, maker_id, status, remark) VALUES (CONCAT('结转-', p_year, '-', p_month), LAST_DAY(CONCAT(p_year,'-',p_month,'-01')), 1, 2, '月末结转'); SET @vid = LAST_INSERT_ID(); INSERT INTO voucher_entry(voucher_id, account_id, direction, amount, summary) SELECT @vid, a.id, CASE WHEN a.type = 5 AND a.balance > 0 THEN 2 ELSE 1 END, ABS(a.balance), '结转损益' FROM account a WHERE a.type = 5 AND a.balance <> 0; INSERT INTO voucher_entry(voucher_id, account_id, direction, amount, summary) SELECT @vid, id, CASE WHEN v_profit > 0 THEN 2 ELSE 1 END, ABS(v_profit), '本年利润' FROM account WHERE code = '4103'; -- 清零损益类科目余额 UPDATE account SET balance = 0 WHERE type = 5; END // DELIMITER ;

调用方式:CALL month_end_close(2024, 6);。这个存储过程的逻辑是先校验再结转,校验不通过直接SIGNAL抛错,不会产生半截数据。LAST_INSERT_ID()拿到刚插入的凭证 ID 用于分录关联。注意存储过程里的事务需要外部调用时开启,或者在过程内START TRANSACTION,MySQL 存储过程支持事务控制但要注意隔离级别。

验证方法:结转后查SELECT * FROM account WHERE type = 5,余额应该全为 0;查voucher表最后一条,借贷应该平衡。如果发现损益类没清零,检查balance字段是否在结转前就是 0,或者type字段值不对。

我自己的习惯是每次改完 Service 层逻辑,先手动跑一遍存储过程校验,再走界面操作,两边结果一致才放心。财务系统这东西,界面点着没问题不代表账对,只有 SQL 查出来的数对上了才算数。希望帮到你。

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

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

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

立即咨询