
简介本资源为基于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-mappingdoPost里手动setCharacterEncodingJDBC 连接自己管。这不是倒退是把「框架帮你做的事」摊开看一遍。我一般建议先用原生 Servlet 把 CRUD 和事务跑通再去对比框架帮你省了什么这个认知差比直接学框架值钱得多。选型上还有一点财务系统对数据一致性要求高MySQL 的 InnoDB 引擎支持事务和行级锁配合 Servlet 层的手动commit/rollback能精确控制到「一张凭证的借贷分录要么全写入要么全回滚」。框架的声明式事务虽然方便但排查问题时你往往不知道事务边界到底在哪原生写法反而让边界清清楚楚。2.2 MySQL 8 安装与建库建表MySQL 安装这块Windows 下推荐用官方 installerLinux 下用 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 父科目ID0为一级, balance DECIMAL(18,2) DEFAULT 0.00 COMMENT 当前余额, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; -- 凭证主表一张凭证一条记录 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 ) ENGINEInnoDB; -- 凭证分录表借贷方明细 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) ) ENGINEInnoDB;建表逻辑说明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 namejdbc/financeDB authContainer typejavax.sql.DataSource maxTotal20 maxIdle10 maxWaitMillis10000 usernameroot passwordyour_password driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/finance_db?useUnicodetrueamp;characterEncodingutf8mb4amp;serverTimezoneAsia/Shanghaiamp;useSSLfalse/ /ContextmaxTotal控制最大连接数财务系统并发不高20 足够maxWaitMillis是获取连接的超时时间设 10 秒避免请求堆积。serverTimezone必须显式指定否则 MySQL 8 会报时区错误。useSSLfalse在本地开发没问题生产环境建议开启并配证书但课设阶段先关掉省事。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;charsetUTF-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); ListVoucherEntryDTO 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 用nameentryAccountId即可。金额用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字段很关键只有status2已过账的凭证才影响余额。删除或反审核时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 加characterEncodingutf8mb4建库建表也用utf8mb4。三处缺一不可只改一处照样乱码。5.2 事务不回滚现象是凭证主表插入了但分录没进去现象保存凭证时报错但voucher表多了一条脏数据。原因DAO 方法内部各自getConnectionService 的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不用equalsequals会比较精度。另外余额更新时用UPDATE account SET balance balance ?让数据库做加法不要查出来在 Java 里加再写回并发下会丢更新。5.4 连接泄漏现象是跑一会儿就报 Too many connections现象系统运行一段时间后所有请求卡死MySQL 报连接数超限。原因Connection、PreparedStatement、ResultSet没有在finally里关闭或者异常路径漏关。解决用 try-with-resources 语法或者严格在finally里按ResultSet → Statement → Connection顺序关闭。连接池配了maxTotal20泄漏 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; -- 计算损益类科目type5净额 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 TRANSACTIONMySQL 存储过程支持事务控制但要注意隔离级别。验证方法结转后查SELECT * FROM account WHERE type 5余额应该全为 0查voucher表最后一条借贷应该平衡。如果发现损益类没清零检查balance字段是否在结转前就是 0或者type字段值不对。我自己的习惯是每次改完 Service 层逻辑先手动跑一遍存储过程校验再走界面操作两边结果一致才放心。财务系统这东西界面点着没问题不代表账对只有 SQL 查出来的数对上了才算数。希望帮到你。本文还有配套的精品资源点击获取