尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Java财务系统实战:凭证管理与会计恒等式数据库建模

Java财务系统实战:凭证管理与会计恒等式数据库建模 简介本资源是一套完整的Java毕业设计项目——企业财务管理系统面向计算机相关专业本科生及Java初学者解决毕业设计选题、系统开发与答辩全流程需求。压缩包共14个文件含3个MP4项目讲解视频覆盖环境部署、部门/职工/工资模块、资产/费用/年终分析等核心功能、2个DOCX中期报告与任务书、2个DOC格式论文与检查表、1个PPT答辩材料、1个SQL数据库脚本、1个ZIP源代码包、3张PNG系统截图及1个TXT说明文档整体大小117.77MB。已有437人学习下载内容结构清晰从系统搭建到模块实现均有详细演示配套视频逐模块讲解开发逻辑与运行要点论文与PPT符合高校规范数据库脚本可直接导入使用源码基于JSPServletMySQL实现具备完整业务闭环与可运行性适合快速复现、二次开发或课程设计参考。1. 这不是又一个“学生管理系统”Java企业级财务系统的真实落地逻辑你下载的这个压缩包里藏着一套能跑通「凭证录入→科目汇总→资产负债表生成→多维度利润分析」全链路的 Java Web 应用。它不是教学演示用的 CRUD 堆砌体而是以《企业会计准则》为隐性约束、用 JSPServletMySQL 构建的轻量级财务内控原型——凭证编号自动生成带校验位、余额自动结转跨月、辅助核算支持部门/项目双维度绑定。对刚学完 JDBC 和 JSP 的同学它是一份可调试、可扩展、能讲清楚「为什么这里用 Filter 而不用 Servlet 拦截登录态」「为什么总账模块必须用事务隔离级别 READ_COMMITTED」的实战样本对正在准备 Java 面试题的开发者它的数据库设计特别是t_voucher_detail与t_account_balance的冗余更新策略和VoucherService中的幂等性控制正是面试官常问的「如何保证财务数据强一致性」的具象答案。别急着解压跑起来——先看清它解决的是什么问题中小企业缺乏专业 ERP 时如何用最小技术栈守住记账底线。2. 从数据库 ER 图到 Java 实体类财务系统建模的不可妥协点财务系统不是普通业务系统它的数据模型必须承载会计恒等式资产负债所有者权益的数学约束。这套代码的database/finance.sql文件里核心表结构不是凭经验拍出来的而是严格对应会计科目体系设计的。2.1 科目表t_account的三级编码与树形结构实现CREATE TABLE t_account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL COMMENT 科目编码如100101, name VARCHAR(64) NOT NULL COMMENT 科目名称, level TINYINT NOT NULL COMMENT 层级1-一级科目2-二级3-三级, parent_id BIGINT DEFAULT NULL COMMENT 父科目ID, is_leaf BOOLEAN DEFAULT TRUE COMMENT 是否末级科目, balance_type ENUM(debit,credit) NOT NULL COMMENT 余额方向借方/贷方, CONSTRAINT uk_code UNIQUE (code) );提示code字段采用定长数字编码非 UUID这是为了支持快速前缀匹配查询。例如查询「应收账款」下所有明细科目只需WHERE code LIKE 1122%。balance_type字段强制约束了借贷方向避免在 Java 层做动态判断——这是财务系统与普通业务系统的根本分界线。2.2 凭证主子表的强一致性保障机制凭证主表t_voucher与明细表t_voucher_detail之间通过外键 级联删除 事务包裹三重保障-- 主表 CREATE TABLE t_voucher ( id BIGINT PRIMARY KEY AUTO_INCREMENT, voucher_no VARCHAR(32) NOT NULL COMMENT 凭证号格式记-2024-0001, date DATE NOT NULL COMMENT 记账日期, description VARCHAR(255) COMMENT 摘要, creator_id BIGINT NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, status ENUM(draft,posted) DEFAULT draft COMMENT 状态草稿/已过账 ); -- 明细表关键外键引用 CHECK 约束 CREATE TABLE t_voucher_detail ( id BIGINT PRIMARY KEY AUTO_INCREMENT, voucher_id BIGINT NOT NULL, account_id BIGINT NOT NULL, debit_amount DECIMAL(18,2) DEFAULT 0.00, credit_amount DECIMAL(18,2) DEFAULT 0.00, remark VARCHAR(100), FOREIGN KEY (voucher_id) REFERENCES t_voucher(id) ON DELETE CASCADE, CHECK (debit_amount 0 AND credit_amount 0), CHECK (debit_amount 0 OR credit_amount 0) -- 借贷必居其一 );2.1.1 为什么CHECK (debit_amount 0 OR credit_amount 0)不可省略这是会计「有借必有贷借贷必相等」原则的数据库层硬约束。如果允许同一行同时填借方和贷方金额会导致t_voucher_detail行数膨胀本该一行记录的借贷分录被拆成两行破坏凭证的原子性。Java 层VoucherService.saveVoucher()方法中对明细列表的校验逻辑必须与该 CHECK 保持一致// VoucherService.java 片段 public void saveVoucher(Voucher voucher) { // 1. 校验借贷平衡前端传入的明细已按单向金额拆分 BigDecimal totalDebit voucher.getDetails().stream() .map(VoucherDetail::getDebitAmount).reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal totalCredit voucher.getDetails().stream() .map(VoucherDetail::getCreditAmount).reduce(BigDecimal.ZERO, BigDecimal::add); if (!totalDebit.equals(totalCredit)) { throw new BusinessException(凭证借贷不平借方合计 totalDebit 贷方合计 totalCredit); } // 2. 开启事务插入主表和明细表 transactionTemplate.execute(status - { voucherMapper.insert(voucher); // 获取主表id voucher.getDetails().forEach(detail - { detail.setVoucherId(voucher.getId()); detailMapper.insert(detail); }); return null; }); }2.3 余额表t_account_balance的增量更新设计财务系统最怕「查余额要遍历所有历史凭证」。本系统采用「按月快照 日增量」模式CREATE TABLE t_account_balance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_id BIGINT NOT NULL, year_month CHAR(6) NOT NULL COMMENT 年月格式202401, opening_balance DECIMAL(18,2) DEFAULT 0.00 COMMENT 期初余额, debit_total DECIMAL(18,2) DEFAULT 0.00 COMMENT 本期借方发生额, credit_total DECIMAL(18,2) DEFAULT 0.00 COMMENT 本期贷方发生额, closing_balance DECIMAL(18,2) DEFAULT 0.00 COMMENT 期末余额, UNIQUE KEY uk_account_month (account_id, year_month) );注意closing_balance不是实时计算得出而是在凭证过账statusposted时由BalanceUpdateJob定时任务或VoucherService.postVoucher()同步更新。这种设计牺牲了毫秒级实时性但换来了报表查询的 O(1) 复杂度——资产负债表生成时直接SELECT closing_balance FROM t_account_balance WHERE year_month202401即可。3. JSP 页面与 Servlet 的职责边界如何让财务操作不出现「页面刷新后重复提交」这套系统用 JSP 作为视图层但绝不是把 Java 代码塞进% %里完事。它的web/目录结构清晰划分了职责/voucher/add.jsp只负责渲染表单/servlet/VoucherServlet承担全部业务逻辑/filter/LoginFilter统一拦截未登录请求。3.1 凭证新增页的防重复提交实现Post-Redirect-Get 模式add.jsp提交到VoucherServlet后者处理完跳转回列表页彻底规避 F5 刷新导致的重复凭证// VoucherServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 解析请求参数使用 Apache Commons FileUpload 处理附件上传 MapString, String[] params request.getParameterMap(); Voucher voucher parseVoucherFromParams(params); // 2. 业务校验与保存已在2.2节详述 voucherService.saveVoucher(voucher); // 3. PRG 模式重定向到列表页而非 forward response.sendRedirect(request.getContextPath() /voucher/list.jsp?msgsave_success); }3.1.1 为什么不用request.getRequestDispatcher(/voucher/list.jsp).forward()forward会让浏览器地址栏仍显示/servlet/VoucherServlet用户刷新时会再次 POST 提交。而sendRedirect使浏览器发起新的 GET 请求地址栏变为/voucher/list.jsp此时刷新只会重新加载列表页不会重复执行保存逻辑。这是财务系统操作安全的最低门槛。3.2 JSP 中的财务专用标签库避免在页面写复杂计算逻辑系统自定义了finance.tld标签库将余额方向判断、金额格式化等逻辑封装为标签!-- list.jsp 片段 -- % taglib prefixfin uri/WEB-INF/tlds/finance.tld % c:forEach items${voucherList} varv tr td${v.voucherNo}/td tdfmt:formatDate value${v.date} patternyyyy-MM-dd//td td${v.description}/td td fin:balanceDirection accountId${v.accountId} amount${v.debitAmount}/ !-- 标签内部根据 account.balance_type 决定显示「借」或「贷」 -- /td tdfin:formatCurrency value${v.amount}//td /tr /c:forEach提示fin:formatCurrency标签内部调用NumberFormat.getCurrencyInstance(Locale.CHINA)确保金额显示为「¥1,234.56」而非「1234.56」。这种封装让 JSP 保持声明式避免出现% new DecimalFormat(#,##0.00).format(amount) %这类易出错的脚本片段。3.3 数据库连接池配置HikariCP 在财务场景下的关键参数src/main/resources/jdbc.properties中的连接池配置不是默认值堆砌# HikariCP 配置非默认值 jdbc.urljdbc:mysql://localhost:3306/finance_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456 # 关键参数财务系统对连接稳定性要求极高 hikari.maximum-pool-size20 hikari.minimum-idle5 hikari.connection-timeout30000 hikari.idle-timeout600000 hikari.max-lifetime1800000 hikari.validation-timeout3000 hikari.connection-test-querySELECT 13.3.1 为什么max-lifetime设为 1800000ms30分钟MySQL 默认wait_timeout为 28800 秒8小时但生产环境防火墙或中间件可能更早断开空闲连接。设为 30 分钟确保连接在 MySQL 主动关闭前被 HikariCP 主动回收并重建避免出现Communications link failure异常。connection-test-querySELECT 1是必须项——财务系统绝不允许因连接失效导致凭证丢失。4. 源代码调试与性能验证三个必须验证的财务核心场景拿到源码后不要只运行mvn tomcat7:run就算完成。以下三个场景是检验系统是否真正可用的试金石每个都需手动验证并观察日志输出。4.1 场景一跨月结转时的期初余额自动继承操作路径在202401月份录入一笔「银行存款」借方 10000 元的凭证 → 过账查看t_account_balance表确认year_month202401的closing_balance10000.00切换到202402月份新增一笔「主营业务收入」贷方 5000 元凭证 → 过账查看t_account_balance表确认year_month202402的opening_balance是否等于202401的closing_balance验证要点若202402的opening_balance为空或为 0说明BalanceUpdateJob未触发或逻辑有误正确行为BalanceUpdateJob在每月 1 日凌晨执行扫描上月所有科目余额批量插入新月份记录opening_balance字段值 上月closing_balance4.2 场景二辅助核算维度部门的多维利润表生成系统支持在凭证明细中绑定「部门」维度t_voucher_detail表有dept_id字段。验证步骤在t_department表插入测试部门INSERT INTO t_department (name) VALUES (销售部),(技术部);录入两笔凭证销售部主营业务收入 10000贷技术部管理费用 3000借访问/report/profitByDept.jsp检查生成的表格是否正确显示部门收入费用利润销售部10000.000.0010000.00技术部0.003000.00-3000.00关键 SQL 验证在 MySQL 客户端执行SELECT d.name AS dept_name, COALESCE(SUM(CASE WHEN a.balance_typecredit THEN vd.credit_amount ELSE 0 END), 0) AS income, COALESCE(SUM(CASE WHEN a.balance_typedebit THEN vd.debit_amount ELSE 0 END), 0) AS expense, COALESCE(SUM(CASE WHEN a.balance_typecredit THEN vd.credit_amount ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN a.balance_typedebit THEN vd.debit_amount ELSE 0 END), 0) AS profit FROM t_voucher_detail vd JOIN t_account a ON vd.account_id a.id JOIN t_department d ON vd.dept_id d.id JOIN t_voucher v ON vd.voucher_id v.id WHERE v.status posted AND v.date BETWEEN 2024-01-01 AND 2024-12-31 GROUP BY d.id, d.name;4.3 场景三高并发凭证录入下的数据库锁表现财务月末结账时多会计可能同时录入凭证。用 JMeter 模拟 50 线程并发提交凭证线程组50 个线程循环 10 次HTTP 请求POST/servlet/VoucherServletBody 为 JSON 凭证数据断言响应状态码 200且响应体包含success:true观察指标Tomcat 日志中VoucherService.saveVoucher()方法的平均耗时应稳定在 80~120msMySQLSHOW ENGINE INNODB STATUS\G中的TRANSACTIONS部分确认无长时间等待锁lock_wait_time 1000ms若出现大量Lock wait timeout exceeded错误需检查t_voucher表的voucher_no字段是否加了唯一索引必须有——凭证号重复是高频冲突点5. 从毕业设计到真实项目三个可立即落地的增强点这套代码的起点是教学但它的架构已具备向生产环境演进的基础。以下三个增强点无需重写核心只需修改少量配置和代码就能显著提升实用性。5.1 替换 JSP 为前后端分离Vue3 Axios 接入方案保留原有VoucherServlet作为 REST API新增/api/v1/voucher接口// VoucherApiController.java RestController RequestMapping(/api/v1) public class VoucherApiController { PostMapping(/voucher) public ResultVOVoucher createVoucher(RequestBody VoucherDTO dto) { Voucher voucher voucherService.convertToEntity(dto); voucherService.saveVoucher(voucher); return ResultVO.success(voucher); } GetMapping(/voucher/{id}) public ResultVOVoucher getVoucher(PathVariable Long id) { return ResultVO.success(voucherService.findById(id)); } }前端 Vue3 组件调用示例script setup import { ref } from vue import { ElMessage } from element-plus import axios from axios const voucherForm ref({ date: new Date().toISOString().split(T)[0], description: , details: [{ accountId: , debitAmount: , creditAmount: }] }) const submitVoucher async () { try { const res await axios.post(/api/v1/voucher, voucherForm.value) ElMessage.success(凭证保存成功) } catch (e) { ElMessage.error(保存失败 e.response?.data?.message) } } /script优势JSP 页面刷新整页的体验被彻底消除凭证录入、附件上传、实时校验全部在单页内完成符合现代财务软件交互习惯。5.2 数据库审计日志记录谁在何时修改了哪张凭证在t_voucher表增加审计字段并创建触发器ALTER TABLE t_voucher ADD COLUMN created_by VARCHAR(64) DEFAULT NULL, ADD COLUMN created_time DATETIME DEFAULT CURRENT_TIMESTAMP, ADD COLUMN updated_by VARCHAR(64) DEFAULT NULL, ADD COLUMN updated_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP; -- 创建审计日志表 CREATE TABLE t_voucher_audit ( id BIGINT PRIMARY KEY AUTO_INCREMENT, voucher_id BIGINT NOT NULL, operator VARCHAR(64) NOT NULL, operation ENUM(insert,update,delete) NOT NULL, old_data TEXT, new_data TEXT, operate_time DATETIME DEFAULT CURRENT_TIMESTAMP );VoucherService.updateVoucher()方法中在更新前获取旧数据并插入审计表public void updateVoucher(Long id, Voucher newVoucher) { Voucher oldVoucher voucherMapper.selectById(id); String oldJson JSON.toJSONString(oldVoucher); // 执行更新... voucherMapper.updateById(newVoucher); // 记录审计日志 VoucherAudit audit new VoucherAudit(); audit.setVoucherId(id); audit.setOperator(getCurrentOperator()); // 从 Session 获取当前登录人 audit.setOperation(update); audit.setOldData(oldJson); audit.setNewData(JSON.toJSONString(newVoucher)); auditMapper.insert(audit); }5.3 Excel 导出优化Apache POI SXSSFWorkbook 流式写入原ExportExcelServlet使用HSSFWorkbook内存式导出万行凭证会 OOM。替换为流式写入// ExportExcelServlet.java protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { ListVoucher vouchers voucherService.listAll(); // 使用 SXSSFWorkbook每 100 行刷入磁盘一次 SXSSFWorkbook workbook new SXSSFWorkbook(100); Sheet sheet workbook.createSheet(凭证列表); // 写入表头 Row headerRow sheet.createRow(0); String[] headers {凭证号,日期,摘要,借方金额,贷方金额}; for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 写入数据避免一次性加载所有数据到内存 int rowNum 1; for (Voucher v : vouchers) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(v.getVoucherNo()); row.createCell(1).setCellValue(new SimpleDateFormat(yyyy-MM-dd).format(v.getDate())); row.createCell(2).setCellValue(v.getDescription()); row.createCell(3).setCellValue(v.getDebitTotal().doubleValue()); row.createCell(4).setCellValue(v.getCreditTotal().doubleValue()); } // 设置响应头触发浏览器下载 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenamevouchers_ System.currentTimeMillis() .xlsx); try (OutputStream out response.getOutputStream()) { workbook.write(out); } finally { workbook.dispose(); // 必须调用释放临时文件 } }本文还有配套的精品资源点击获取
返回列表