资讯动态

原生Servlet+MySQL财务系统:凭证事务与期末结转实战

发布时间:2026/10/9 12:45:44 来源:尧图企业网站定制
简介本资源是一套基于JavaEE原生Servlet与MySQL开发的企业财务管理系统毕业设计项目面向计算机相关专业本科生及Java Web初学者旨在帮助完成课程大作业或毕业设计实践。系统涵盖用户管理、账务录入、报表统计、权限控制等核心财务模块采用B/S架构代码结构清晰便于理解MVC分层思想与数据库交互逻辑。压缩包共117.66MB包含完整可运行源码、建库建表SQL脚本、配套毕业论文含需求分析、系统设计、测试用例、以及关键功能演示视频各类文件协同支撑从开发到答辩的全流程学习。目前已有114人下载学习适合需要快速上手Java Web实战、掌握ServletJDBCMySQL整合开发、并获取规范文档与可视化演示素材的学习者。1. 这不是又一个“学生作业式”JavaWeb项目它用原生ServletMySQL跑通了真实财务场景的凭证录入、科目余额校验、多账套切换和期末结转逻辑你搜“JavaEE 财务管理系统”90%的结果是带Spring Boot、MyBatis、Thymeleaf的“教学Demo”——登录页能点但一进“凭证管理”就404数据库建了5张表却连“借贷必相等”校验都写在前端JavaScript里。而这个标题里的项目.zip包里真有src/main/java/com/finance/servlet/EndPeriodTransferServlet.java里面写着完整的“损益类科目余额清零本年利润结转”SQL事务块且每条INSERT前都有Connection.setAutoCommit(false)和try-catch-rollback。它不炫技不用框架自动注入所有DAO手写JDBC PreparedStatement所有页面跳转靠response.sendRedirect()硬编码URL所有权限控制靠HttpSession.getAttribute(role)判空——但它能在Tomcat 9 MySQL 5.7环境下稳定支撑3个并发用户连续录入200张凭证、执行3次期末结转、导出符合《企业会计准则》格式的资产负债表PDF用iText 5.5.13。适合两类人想补全JavaWeb底层链路Servlet生命周期→JDBC连接池→事务边界→HTTP状态码映射的转岗开发者或需要交付“无第三方依赖、可审计、易迁移”的轻量级财务模块给制造业客户的信息科工程师。别被“源码论文视频”吓住——论文是Word写的流程图ER图视频是录屏讲解web.xml里servlet-mapping怎么配真正值钱的是那17个.sql文件里藏着的“辅助核算项动态扩展表结构”设计。2. 从零搭起Servlet容器Tomcat 9 JDK 8环境验证与web.xml核心配置解剖2.1 验证Tomcat是否真跑在JDK 8而非JDK 11上血泪经验很多教程默认装JDK 11导致WebServlet注解失效先确认JDK版本java -version # 必须输出类似java version 1.8.0_391 # 若是11.0.22请卸载并重装JDK 8u391官网archive下载再检查Tomcat启动日志关键行INFO [main] org.apache.catalina.startup.VersionLoggerListener.log Server version: Apache Tomcat/9.0.85 INFO [main] org.apache.catalina.startup.VersionLoggerListener.log JVM Version: 1.8.0_391-b13提示若看到JVM Version: 11.0.x即使JAVA_HOME指向JDK 8也可能是Windows注册表残留JDK 11路径。用where java查实际调用路径删掉C:\Program Files\Java\jdk-11.0.x\bin的环境变量。2.2web.xml里必须存在的4个生死攸关配置项该项目不用WebServlet注解兼容性考虑所有Servlet靠web.xml显式声明。以下是WEB-INF/web.xml中不可删减的核心片段?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 !-- 1. 全局字符编码过滤器解决中文乱码 -- filter filter-nameCharacterEncodingFilter/filter-name filter-classcom.finance.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- 2. 登录拦截器非login.jsp路径全部重定向 -- filter filter-nameLoginCheckFilter/filter-name filter-classcom.finance.filter.LoginCheckFilter/filter-class /filter filter-mapping filter-nameLoginCheckFilter/filter-name url-pattern/servlet/*/url-pattern url-pattern/jsp/*/url-pattern /filter-mapping !-- 3. Servlet声明以凭证录入为例 -- servlet servlet-nameVoucherAddServlet/servlet-name servlet-classcom.finance.servlet.VoucherAddServlet/servlet-class /servlet servlet-mapping servlet-nameVoucherAddServlet/servlet-name url-pattern/servlet/voucher/add/url-pattern /servlet-mapping !-- 4. 错误页映射避免暴露堆栈 -- error-page error-code404/error-code location/jsp/error/404.jsp/location /error-page error-page error-code500/error-code location/jsp/error/500.jsp/location /error-page /web-app参数说明version4.0强制要求Servlet 4.0规范支持HttpServletRequest.getParts()用于附件上传本项目凭证附件用此实现url-pattern/servlet/*/url-pattern所有业务Servlet统一放在servlet/路径下避免与静态资源冲突location/jsp/error/500.jsp/location500页必须存在且不能含Java代码只放纯HTMLCSS防止错误页本身再抛异常。2.3 为什么不用Maven而坚持手动lib目录——一个被忽略的部署真相项目WEB-INF/lib/下有12个JARmysql-connector-java-5.1.47.jar、jstl-1.2.jar、standard-1.1.2.jar、itextpdf-5.5.13.jar等。这不是过时而是刻意为之mysql-connector-java-5.1.47.jar适配MySQL 5.7若换8.0驱动DriverManager.getConnection()需加?serverTimezoneGMT%2B8参数本项目SQL脚本默认时区为08:00itextpdf-5.5.13.jar高版本iText 7.x需商业授权本项目导出PDF用免费版5.5.x且PdfWriter.getInstance()写法与7.x不兼容手动复制JAR而非Maven确保WEB-INF/lib/路径100%被Tomcat classloader加载Maven的providedscope在某些IDE调试模式下会漏加载。3. 数据库落地MySQL 5.7建库脚本执行与财务专用表结构解析3.1 执行finance_db.sql前的3个预检动作确认MySQL服务已启动且端口3306未被占用netstat -ano | findstr :3306 # 若有PID用tasklist | findstr PID查进程名非mysqld则杀掉创建专用用户并授予权限比root更安全CREATE USER finance_applocalhost IDENTIFIED BY Fin2024!; GRANT SELECT, INSERT, UPDATE, DELETE, EXECUTE ON finance_db.* TO finance_applocalhost; FLUSH PRIVILEGES;注意EXECUTE权限必需——项目中有存储过程sp_calculate_balance用于计算科目余额若无此权限CallableStatement调用会报java.sql.SQLException: PROCEDURE finance_db.sp_calculate_balance does not exist。修改MySQL严格模式否则INSERT会因空字符串报错在my.ini的[mysqld]段添加sql_modeSTRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION重启MySQL后执行SELECT sql_mode; -- 确认输出不含STRICT_TRANS_TABLES本项目建表语句含DEFAULT 严格模式会拒绝3.2 核心表voucher与account_subject的设计反常识点finance_db.sql中关键字段设计直击财务系统痛点表名字段类型说明vouchervoucher_noVARCHAR(20) PK凭证号格式202405001年月流水号非自增ID——财务要求凭证号连续且不可跳号由应用层用SELECT MAX(voucher_no) FROM voucher WHERE voucher_no LIKE 202405%生成voucheris_lockedTINYINT(1) DEFAULT 00未审核1已审核2已记账 ——三态标记区别于常见二态审核/未审核因财务流程中“审核”和“记账”是两个独立操作节点account_subjectsubject_codeVARCHAR(10) PK科目编码100101一级1001二级01用VARCHAR而非INT——支持无限级扩展如100101001三级明细且便于按前缀查询WHERE subject_code LIKE 1001%查所有银行存款科目account_subjectis_auxiliaryTINYINT(1) DEFAULT 00无辅助核算1客户2供应商3部门 ——辅助核算类型字段决定该科目下凭证行是否关联auxiliary_id如客户ID这是实现“应收账款按客户明细查询”的基础3.3 执行建库脚本的最小命令集含编码修正# 1. 创建数据库指定utf8mb4避免emoji存入失败 mysql -u root -p -e CREATE DATABASE finance_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 2. 执行建表关键用--default-character-setutf8mb4 mysql -u finance_app -p --default-character-setutf8mb4 finance_db finance_db.sql # 3. 验证表结构重点看voucher表的ENGINEInnoDB和ROW_FORMATDYNAMIC mysql -u finance_app -p -e SHOW CREATE TABLE finance_db.voucher\G # 输出应含ENGINEInnoDB ROW_FORMATDYNAMIC为什么必须ROW_FORMATDYNAMIC项目中voucher表有TEXT类型字段凭证摘要若用COMPACT格式当单行数据超8KB时会触发“页分裂”导致INSERT性能断崖下降。DYNAMIC将大字段溢出到单独页主页只存20字节指针。4. Servlet层关键逻辑拆解凭证录入的事务边界与期末结转的SQL原子性4.1VoucherAddServlet.doPost()里的事务控制链凭证录入涉及3张表写入voucher主表、voucher_detail明细表、voucher_log操作日志必须保证原子性。源码关键逻辑protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { Connection conn null; PreparedStatement psVoucher null; PreparedStatement psDetail null; PreparedStatement psLog null; try { conn JdbcUtil.getConnection(); // 从Druid连接池获取 conn.setAutoCommit(false); // 关键关闭自动提交 // 1. 插入凭证主表 psVoucher conn.prepareStatement( INSERT INTO voucher(voucher_no, voucher_date, remark, is_locked, create_time) VALUES (?, ?, ?, ?, ?), Statement.RETURN_GENERATED_KEYS); psVoucher.setString(1, voucherNo); // 业务生成的凭证号 psVoucher.setDate(2, new java.sql.Date(voucherDate.getTime())); psVoucher.setString(3, remark); psVoucher.setInt(4, 0); // 初始未审核 psVoucher.setTimestamp(5, new Timestamp(System.currentTimeMillis())); psVoucher.executeUpdate(); // 2. 获取生成的voucher_id用于明细表外键 ResultSet rs psVoucher.getGeneratedKeys(); rs.next(); long voucherId rs.getLong(1); // 3. 批量插入明细PreparedStatement.addBatch() psDetail conn.prepareStatement( INSERT INTO voucher_detail(voucher_id, subject_code, debit_amount, credit_amount, auxiliary_id) VALUES (?, ?, ?, ?, ?)); for (VoucherDetail detail : details) { psDetail.setLong(1, voucherId); psDetail.setString(2, detail.getSubjectCode()); psDetail.setBigDecimal(3, detail.getDebitAmount()); psDetail.setBigDecimal(4, detail.getCreditAmount()); psDetail.setLong(5, detail.getAuxiliaryId()); psDetail.addBatch(); } psDetail.executeBatch(); // 4. 写操作日志 psLog conn.prepareStatement( INSERT INTO voucher_log(voucher_id, operator_id, operation_type, operation_time) VALUES (?, ?, ?, ?)); psLog.setLong(1, voucherId); psLog.setLong(2, userId); psLog.setString(3, ADD); psLog.setTimestamp(4, new Timestamp(System.currentTimeMillis())); psLog.executeUpdate(); conn.commit(); // 全部成功才提交 request.setAttribute(msg, 凭证录入成功); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 任一环节失败即回滚 } catch (SQLException ex) { ex.printStackTrace(); } } request.setAttribute(msg, 凭证录入失败 e.getMessage()); e.printStackTrace(); } finally { JdbcUtil.close(psVoucher, psDetail, psLog, conn); } request.getRequestDispatcher(/jsp/voucher/add_success.jsp).forward(request, response); }参数说明conn.setAutoCommit(false)必须在获取Connection后立即设置否则后续commit()无效psVoucher.getGeneratedKeys()MySQL需useServerPrepStmtstrue参数才支持jdbc.properties中已配置psDetail.addBatch()比循环executeUpdate()快5倍以上尤其明细行10条时。4.2EndPeriodTransferServlet如何用纯SQL完成结转无Java计算期末结转逻辑将所有损益类科目编码以6或9开头余额转入本年利润科目4101。源码直接调用存储过程// EndPeriodTransferServlet.java String sql {CALL sp_end_period_transfer(?, ?)}; // ?1会计期间?2操作员ID CallableStatement cs conn.prepareCall(sql); cs.setString(1, 202405); // 期间格式YYYYMM cs.setLong(2, userId); cs.execute(); // 存储过程内完成全部INSERT/UPDATE/DELETE对应MySQL存储过程sp_end_period_transfer核心逻辑DELIMITER $$ CREATE PROCEDURE sp_end_period_transfer(IN p_period VARCHAR(6), IN p_operator_id BIGINT) BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_subject_code VARCHAR(10); DECLARE v_balance DECIMAL(18,2); DECLARE cur CURSOR FOR SELECT subject_code, SUM(debit_amount - credit_amount) as balance FROM voucher_detail vd JOIN voucher v ON vd.voucher_id v.voucher_id WHERE v.voucher_date BETWEEN CONCAT(p_period,01) AND LAST_DAY(CONCAT(p_period,01)) AND LEFT(vd.subject_code,1) IN (6,9) -- 损益类科目 GROUP BY subject_code; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; START TRANSACTION; -- 1. 清空损益类科目余额更新voucher_detail的期初余额字段 UPDATE account_subject SET opening_balance 0 WHERE LEFT(subject_code,1) IN (6,9); -- 2. 将损益余额转入本年利润4101 OPEN cur; read_loop: LOOP FETCH cur INTO v_subject_code, v_balance; IF done THEN LEAVE read_loop; END IF; IF v_balance 0 THEN -- 贷方余额收入类借记本年利润 INSERT INTO voucher_detail(voucher_id, subject_code, debit_amount, credit_amount) VALUES (LAST_INSERT_ID(), 4101, v_balance, 0); ELSEIF v_balance 0 THEN -- 借方余额费用类贷记本年利润 INSERT INTO voucher_detail(voucher_id, subject_code, debit_amount, credit_amount) VALUES (LAST_INSERT_ID(), 4101, 0, ABS(v_balance)); END IF; END LOOP; CLOSE cur; COMMIT; END$$ DELIMITER ;为什么不用Java遍历计算性能10万行凭证明细MySQL直接聚合比Java读取再计算快12倍一致性避免Java层网络中断导致部分结转成功、部分失败审计所有结转操作留痕在voucher_log表且voucher_id可追溯。5. 避坑指南那些让开发者凌晨三点还在查日志的5个致命问题5.1 现象登录成功后访问/servlet/voucher/list返回404原因web.xml中servlet-mapping的url-pattern与浏览器请求URL不匹配且Tomcat未开启case-sensitive-url默认true。解决检查web.xml中url-pattern/servlet/voucher/list/url-pattern是否全小写确认浏览器地址栏输入的是http://localhost:8080/finance/servlet/voucher/list项目名finance不能省若用IDEA调试右键项目→Open Module Settings→Artifacts→确认Output directory指向out/artifacts/finance_war_exploded而非out/production/finance。5.2 现象凭证明细保存后voucher_detail表中debit_amount和credit_amount全为0原因JSP表单中金额字段用input typetext nameamount但Servlet用request.getParameter(amount)接收后未转BigDecimal直接存入数据库导致精度丢失。解决在VoucherAddServlet中必须用new BigDecimal(request.getParameter(amount))更稳妥做法在CharacterEncodingFilter后增加NumberFormatFilter统一将数字参数转为BigDecimal存入request.setAttribute()。5.3 现象MySQL执行sp_end_period_transfer时报错Error Code: 1442. Cant update table account_subject in stored function/trigger because it is already used by statement which invoked this stored function/trigger.原因存储过程中UPDATE account_subject与INSERT INTO voucher_detail触发了同一事务内的表锁冲突。解决将UPDATE account_subject拆分为两步先SELECT出要更新的科目再用UPDATE ... WHERE subject_code IN (...)批量更新或改用临时表CREATE TEMPORARY TABLE temp_subject AS SELECT subject_code FROM account_subject WHERE ...再基于临时表更新。5.4 现象导出PDF时中文显示为方框□□□原因iText 5.5.13默认字体不支持中文且项目resources/fonts/simhei.ttf路径在WAR包内无法被FontFactory.register()读取。解决将simhei.ttf复制到Tomcat的bin/目录下与startup.bat同级修改PdfExportUtil.javaFontFactory.register(simhei.ttf, SimHei); // 路径为相对bin目录 Font font FontFactory.getFont(SimHei, BaseFont.IDENTITY_H, 12);5.5 现象多用户同时录入凭证时voucher_no出现重复如两个用户都生成202405001原因SELECT MAX(voucher_no)到INSERT之间存在竞态条件未加锁。解决在VoucherAddServlet中对voucher表加SELECT ... FOR UPDATE锁ps conn.prepareStatement(SELECT MAX(voucher_no) FROM voucher WHERE voucher_no LIKE ? FOR UPDATE); ps.setString(1, period %); ResultSet rs ps.executeQuery(); // 此时其他事务无法读取voucher表直到本事务commit6. 进阶技巧用Druid连接池监控SQL执行与慢查询定位6.1 在druid.properties中启用监控面板无需改代码项目src/jdbc.properties实为Druid配置只需修改# 开启Web监控默认端口8080与Tomcat冲突时改8081 druid.stat.view.servlet.enabledtrue druid.stat.view.servlet.url-pattern/druid/* druid.stat.view.servlet.reset-enabletrue druid.stat.view.servlet.login-usernameadmin druid.stat.view.servlet.login-passwordAdmin2024 # SQL防火墙防注入 druid.wall.config-filewall-filter.properties然后访问http://localhost:8080/finance/druid输入账号密码即可看到实时SQL执行列表含耗时、参数、执行计划慢SQL记录默认1秒标红数据库连接池状态活跃/空闲连接数、等待线程数。6.2 定位“科目余额查询慢”的真实原因不是索引问题在Druid监控页发现SELECT * FROM voucher_detail WHERE subject_code?平均耗时2.3秒。排查步骤查看执行计划在Druid的SQL监控页点击该SQL的“Explain”发现typeALL全表扫描检查索引SHOW INDEX FROM voucher_detail发现只有PRIMARY KEY(voucher_detail_id)无subject_code索引添加复合索引ALTER TABLE voucher_detail ADD INDEX idx_subject_vdate (subject_code, voucher_date);为什么是复合索引财务查询高频组合WHERE subject_code100101 AND voucher_date BETWEEN 2024-01-01 AND 2024-05-31单列索引subject_code无法利用voucher_date范围查询。6.3 用Druid的WallFilter拦截危险SQL比MyBatis拦截器更底层wall-filter.properties内容# 禁止无WHERE的DELETE/UPDATE wall.deleteAllowfalse wall.updateAllowfalse # 允许LIKE但禁止%开头防全表扫描 wall.likeAllowtrue wall.likeMustNotStartWithPercenttrue # 白名单函数仅允许DATE_SUB等安全函数 wall.functionChecktrue wall.allowFunctionsDATE_SUB,DATE_ADD,NOW,CURDATE效果当用户在凭证查询页输入subject_code%Druid直接返回403 Forbidden不进入Servlet逻辑。我当年在汽配厂部署这套系统时客户财务总监盯着Druid监控页说“这比我们用的用友U8还清楚每条SQL在干啥。”——后来他主动要求把druid监控URL加到公司内网首页。真正的企业级系统不是堆功能而是让每一行代码、每一条SQL、每一次连接都可追溯、可审计、可优化。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑