简介:面向Java EE初学者与课程设计、毕业设计开发者,这套原生Servlet+MySQL企业财务管理系统提供了一套可直接运行、可拆解学习的完整项目方案,帮助理解账务处理、报表生成、预算控制、资产与成本分析等模块的落地实现。压缩包共14个文件、约117.82MB,包含项目源码压缩包、数据库SQL脚本、论文与任务书等Word文档、答辩PPT、项目截图以及3段项目辅导视频,覆盖从环境搭建、数据库创建到各功能模块讲解的完整链路。已有91人学习,适合需要参照成熟项目结构完成选题开发或快速掌握Servlet分层开发与MySQL表设计的读者,视频演示也能降低部署与调试门槛。
1. 从一张对不上的报销单说起:这套原生 Servlet 财务系统到底解决什么问题
很多企业财务系统的第一版,都是被一张对不上的报销单逼出来的。业务部门说钱付了,财务说账上没这笔,出纳说凭证号重复了,最后只能翻 Excel 和聊天记录对账。这套基于原生 Servlet + MySQL 的企业财务管理系统设计与实现,瞄准的就是这个场景:把凭证、科目、报销、报表这几件事收进一个统一入口,让每一笔钱都有单据、有科目、有操作人。它适合正在做财务管理系统毕设或课程设计的同学,也适合需要一套轻量可交付方案、不想被重型框架绑住的从业者。原生 Servlet 不是怀旧,而是把请求链路、事务边界、权限过滤全部摊开给你看,出了问题能定位到具体一行,这在财务这种“错一分都要查半天”的领域反而是优势。
2. 需求与选型:为什么财务系统不能只做增删改查
2.1 先把业务需求翻译成可验收的功能点
财务系统的需求如果只写成“用户管理、凭证管理、报表管理”,最后做出来一定是个带登录的表格编辑器。我一般会先把需求拆成三类:记账类、审核类、查询类。记账类包括凭证录入、科目维护、报销单登记;审核类包括凭证审核、报销审批、期间结账;查询类包括科目余额表、明细账、利润表。每一类都要明确“谁能做、做完之后数据变成什么状态”。
以凭证为例,一张凭证至少要有凭证字号、日期、摘要、借贷方明细、制单人、审核状态。录入后是“待审核”,审核通过才是“已记账”,已记账的凭证不允许直接改,只能红冲。这个状态机如果不提前定清楚,后面代码里到处是 if 判断,改一个状态能牵出五个 bug。通常的做法是给凭证主表加一个 status 字段,用 0/1/2 表示待审、已审、已红冲,所有业务操作先校验状态再执行。
报销单同理,提交、部门审批、财务复核、付款完成,每一步都要留痕。需求文档里把这些状态和流转条件写清楚,验收时才有依据,而不是答辩时被问“你这个审批到底谁批的”答不上来。
2.2 技术选型:原生 Servlet 加 JDBC 的取舍
这套方案的技术栈是 Servlet + JSP + JDBC + MySQL,部署在 Tomcat 上。有人会问为什么不用 Spring Boot,答案很实际:毕设和课设的评审重点往往在“你是否理解请求怎么进来、事务怎么控制、连接怎么管”,框架把这些都封装了,反而讲不清。原生 Servlet 的 doGet/doPost 就是入口,Filter 就是权限和编码的统一关卡,JDBC 的 Connection 就是事务边界,每一层都看得见。
选型上还有几个具体决定。数据库访问不引 ORM,用 PreparedStatement 手写 SQL,好处是 SQL 可控、能讲清楚索引怎么用,坏处是重复代码多,所以我会抽一个 BaseDao 做通用增删改查。页面用 JSP 加 JSTL,不搞前后端分离,因为财务系统页面以表单和表格为主,服务端渲染反而少一层联调。连接池用 Druid 或者 Tomcat 自带的 JDBC Pool,不要每次请求都 DriverManager.getConnection,那是血泪经验,并发一上来连接数直接爆。
<!-- web.xml 中统一编码与权限过滤,避免每个 Servlet 重复写 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>com.finance.filter.EncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这段配置的作用是把字符编码统一收口,财务系统里科目名称、摘要经常有中文,漏配一个地方就是乱码。参数 encoding 固定 UTF-8,url-pattern 用 /* 覆盖所有请求。权限过滤器同理,按角色拦截 /admin/* 和 /voucher/audit 这类路径,比在每个 Servlet 里写 session 判断可靠得多。
2.3 数据库设计:科目表、凭证表、明细表的关系
财务系统的数据模型核心是“科目树 + 凭证头 + 凭证分录”。科目表 account 存科目编码、名称、父级编码、科目类型(资产、负债、权益、成本、损益),用 parent_code 自关联形成树。凭证主表 voucher 存凭证字号、日期、制单人、审核人、状态。分录表 voucher_entry 存凭证 ID、科目编码、借方金额、贷方金额、摘要。
关键约束有三条:同一张凭证的借方合计必须等于贷方合计;已记账凭证的分录不可修改;科目编码唯一且符合层级规则。前两条在 Service 层用事务保证,第三条在数据库加唯一索引。金额字段用 DECIMAL(18,2),不要用 FLOAT,浮点误差在财务里是灾难。
CREATE TABLE voucher_entry ( id BIGINT PRIMARY KEY AUTO_INCREMENT, voucher_id BIGINT NOT NULL, account_code VARCHAR(20) NOT NULL, summary VARCHAR(200), debit DECIMAL(18,2) DEFAULT 0.00, credit DECIMAL(18,2) DEFAULT 0.00, KEY idx_voucher (voucher_id), KEY idx_account (account_code) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;分录表是查询最频繁的表,科目余额表和明细账都要扫它,所以 voucher_id 和 account_code 各建一个普通索引。debit 和 credit 分开存而不是存一个带正负的金额,是为了报表汇总时直接 SUM,不用判断方向。外键约束在毕设环境里可以加,但生产环境通常靠应用层保证,这里按教学场景保留逻辑关联即可。
3. 总体架构与模块划分:四层结构怎么落到包和目录
3.1 分层结构:Servlet 只做调度,Service 管事务
整个系统按表现层、控制层、业务层、数据访问层划分。JSP 在 WEB-INF 下,只能通过 Servlet 转发访问,防止用户直接敲 URL 绕过权限。Servlet 负责收参数、调 Service、转发页面,不写业务逻辑。Service 里开事务、校验状态、组装数据。Dao 只做 SQL 执行和结果映射。这个划分听起来像教科书,但真正落地时最容易翻车的是“Servlet 里顺手写了一段 SQL”,所以我会在代码评审时专门搜 Servlet 包里有没有 Connection 关键字。
包结构建议按业务域分而不是按技术分:com.finance.voucher、com.finance.account、com.finance.report、com.finance.user,每个域下再分 servlet、service、dao、entity。这样改凭证功能时不用在四个大包里来回跳。
3.2 核心模块职责与调用关系
用户与权限模块负责登录、角色分配、菜单过滤。角色分制单员、审核员、财务主管,制单员不能审核自己做的凭证,这条规则在 Service 层校验,不依赖前端隐藏按钮。凭证模块负责录入、修改、审核、红冲,是系统的心脏。科目模块维护科目树和期初余额。报销模块处理报销单流转。报表模块生成科目余额表、明细账、利润表。
调用关系上,凭证审核要同时更新凭证状态和写操作日志,这两个动作必须在一个事务里。报销付款要生成凭证,也是跨模块事务。我一般把事务控制放在 Service 方法上,用 try-catch 手动 commit/rollback,因为原生 JDBC 没有声明式事务,手动写反而清楚。
public boolean auditVoucher(Long voucherId, String auditor) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 Voucher v = voucherDao.findById(conn, voucherId); if (v == null || v.getStatus() != 0) { conn.rollback(); return false; // 只有待审核状态才能审 } voucherDao.updateStatus(conn, voucherId, 1, auditor); logDao.insert(conn, auditor, "AUDIT", voucherId); conn.commit(); return true; } catch (Exception e) { DBUtil.rollbackQuietly(conn); throw new RuntimeException("审核失败", e); } finally { DBUtil.close(conn); } }这段代码的关键点是 setAutoCommit(false) 之后,所有 Dao 方法必须共用同一个 Connection,所以 Dao 方法签名里要传 conn,不能各自去连接池取。updateStatus 和 insert 日志要么都成功要么都回滚,这就是财务系统里“审核了但日志没记”这类玄学问题的根治办法。参数 auditor 从 session 里取当前登录人,不要从前端传,否则可以伪造审核人。
3.3 请求链路与权限过滤
一个典型请求从浏览器到数据库要经过:EncodingFilter 设编码,AuthFilter 校验 session 和角色,Servlet 收参数并做基础校验,Service 开事务调多个 Dao,最后转发 JSP 或重定向。重定向用 redirect 防止刷新重复提交,查询用 forward 保留请求数据。
权限过滤的粒度按 URL 前缀加角色判断。比如 /voucher/audit 只允许审核员和主管,/voucher/add 只允许制单员。Filter 里读 session 中的 role,不匹配就跳 403 页面。这里有个常见误用:把权限判断写在 JSP 里用 c:if 隐藏按钮,结果用户直接构造请求照样能提交,所以服务端必须再校验一次。
4. 关键功能实现:凭证、报表与报销的落地细节
4.1 凭证录入与借贷平衡校验
凭证录入页面是一个动态表格,用户加行、选科目、填金额。提交时 Servlet 收数组参数,Service 先校验借贷合计相等,再校验科目存在且是末级科目,最后在一个事务里插主表和分录。借贷不平衡直接返回错误提示,不落库。
BigDecimal debitSum = BigDecimal.ZERO; BigDecimal creditSum = BigDecimal.ZERO; for (VoucherEntry e : entries) { debitSum = debitSum.add(e.getDebit()); creditSum = creditSum.add(e.getCredit()); } if (debitSum.compareTo(creditSum) != 0) { throw new BizException("借贷不平衡:借 " + debitSum + ",贷 " + creditSum); }金额比较一定用 compareTo 而不是 equals,BigDecimal 的 equals 会比较 scale,1.0 和 1.00 不相等,这是踩过的坑。分录至少要有两条,且不能全借全贷,这些校验放在 Service 入口,别等数据库报错再处理。
4.2 科目余额表与明细账的查询优化
科目余额表按科目汇总期初、本期借方、本期贷方、期末。明细账按科目列出一段时间内的每笔分录和余额。这两个报表是答辩时最容易被追问性能的地方。做法是给 voucher 表的日期字段建索引,分录表按 voucher_id 关联,查询时先按日期范围过滤凭证,再汇总分录。
如果数据量不大,直接 SQL 聚合即可;如果分录上百万,通常的做法是加一张余额快照表,每月结账时把余额固化,查询时用快照加当月发生额。毕设阶段数据量小,但把这个思路讲出来,评审会认为你考虑过扩展性。
4.3 报销单流转与状态机
报销单表存单号、申请人、部门、金额、事由、状态、当前审批人。状态流转是:草稿→待部门审批→待财务复核→待付款→已完成,任一环节可驳回回到草稿。每次流转写一条审批记录,记录操作人、时间、意见。付款完成时调用凭证服务生成一张付款凭证,科目取管理费用和银行存款,这一步是跨模块事务,要么报销状态变付款完成且凭证生成,要么都回滚。
状态机的实现不要用一堆 if-else 散在各处,我一般用一个 Map 定义允许的流转,比如 PENDING_DEPT 只能到 PENDING_FINANCE 或 REJECTED,Service 里查表判断,非法流转直接拒绝。这样加一个审批环节只改配置,不改逻辑。
5. 避坑与排查:财务系统最容易翻车的五个点
5.1 并发审核导致重复记账
现象是同一张凭证被两个人同时点审核,状态被改两次,日志写两条。原因是先查状态再更新,查和更新之间没有锁。解决是在 update 语句里带状态条件,UPDATE voucher SET status=1 WHERE id=? AND status=0,根据影响行数判断是否成功,影响 0 行说明已被别人处理。
5.2 事务没生效,一半数据落库
现象是凭证主表插入了,分录没插入,或者审核状态改了日志没写。原因多半是 Dao 各自从连接池取连接,不在同一个事务里。解决是所有参与同一事务的 Dao 方法必须接收外部传入的 Connection,Service 统一开关事务,并且异常时 rollback,finally 里 close。
5.3 中文乱码在导出时集中爆发
页面显示正常,导出 Excel 或下载报表时中文变问号。原因是响应头没设编码,或者 JSP 的 pageEncoding 和 contentType 不一致。解决是统一在 Filter 里设 request/response 编码,导出时设response.setContentType("application/vnd.ms-excel;charset=UTF-8"),并在输出流写入前 flush 正确。
5.4 金额精度丢失
用 double 存金额,汇总后出现 0.01 的偏差,对账永远对不平。原因是浮点二进制表示不精确。解决是数据库用 DECIMAL,Java 用 BigDecimal,加减用 add/subtract,比较用 compareTo,除法指定 scale 和舍入模式。
5.5 权限只在前端做
按钮隐藏了,但接口没拦,用户用工具直接发请求就能越权审核。原因是权限判断写在 JSP 的 c:if 里。解决是 AuthFilter 按 URL 和角色拦截,Service 里再校验“不能审核自己制的凭证”这类业务规则,双层保险。
6. 验收与答辩:怎么证明这套系统真的能用
验收不是把页面点一遍,而是按业务链路走通并留下证据。我一般准备一张验收清单,按角色分场景:制单员登录建一张借贷平衡的凭证,提交后状态为待审核;审核员登录审核通过,凭证变已记账,操作日志多一条;再尝试修改已记账凭证,系统应拒绝;制单员提交一张报销单,部门审批、财务复核、付款完成,检查是否自动生成付款凭证;最后打开科目余额表和明细账,核对金额与手工计算一致。
答辩时最容易被问的三个问题提前准备好答案:事务怎么控制的,拿审核方法那段代码讲 Connection 传递和 commit/rollback;借贷平衡在哪校验的,讲 Service 里的 BigDecimal 汇总;权限怎么防越权,讲 Filter 加 Service 双层。演示时故意制造一次借贷不平衡和一次重复审核,让系统报错,比一路顺畅更有说服力,因为这说明你处理过异常路径。
| 验收项 | 操作 | 预期结果 |
|---|---|---|
| 凭证借贷校验 | 录入借方 100、贷方 90 | 提示不平衡,不落库 |
| 重复审核 | 两个会话同时审核同一凭证 | 只有一个成功,日志一条 |
| 已记账修改 | 修改已审核凭证 | 拒绝并提示 |
| 报销转凭证 | 报销付款完成 | 生成对应付款凭证 |
| 报表核对 | 科目余额表对比明细账 | 期初加发生额等于期末 |
从那以后我每次带这类财务项目,都会在开发前先把状态机和验收清单定死,代码可以慢慢写,状态流转和验收口径一旦含糊,后面全是返工。希望这套结构和实现细节能帮你把毕设或交付方案稳稳落地,需要完整源码和数据库脚本的,可以按项目名去找对应的资源包,照着模块顺序读一遍再改,比从零搭省太多时间。
本文还有配套的精品资源,点击获取