简介:本资源是一套基于Java EE原生Servlet与MySQL开发的企业级财务管理系统完整实现方案,面向Java Web初学者、课程设计学生及中小型系统开发者,解决企业账务处理、资产/成本管理、财务分析与报表生成等核心财务管理需求。压缩包共14个文件,含3个MP4项目辅导视频(覆盖部署、模块开发与年终分析)、3张PNG系统界面截图(登录、信息管理等关键页)、2个DOC/DOCX中期报告与毕业论文文档、1个PPT答辩材料、1个SQL数据库脚本、1个源码ZIP包及1个TXT说明文件,总大小117.82MB,结构清晰、开箱即用。已有91人学习下载,资源提供从环境搭建、数据库初始化、源码运行到功能演示的全流程支持,尤其视频分模块讲解资产类别、职工工资、部门管理等6大业务逻辑,配合论文与代码注释,极大降低理解门槛,是掌握传统Java Web三层架构实战落地的优质教学范例。
1. 为什么还在用原生 Servlet 写企业财务管理系统?——不是守旧,是控得住、验得清、讲得明
你可能刚在毕设选题表里看到“企业财务管理系统”就皱眉:Spring Boot 不香吗?Vue 前端不酷吗?为什么这份资源坚持用原生 Servlet + MySQL?答案很实在:它专为「需要完整呈现设计逻辑、能独立部署验证、答辩时能逐行解释每处数据流向」的学生和初阶工程师准备。这不是技术降级,而是教学级交付的刚需——没有自动装配、没有隐式事务、没有拦截器黑匣子,每一笔凭证录入、每一级科目校验、每一个报表汇总,都暴露在 doGet/doPost 的代码路径里。某高校连续三年把这套结构作为《Java Web 综合实训》标准参考方案,原因就一条:学生交上去的不是“跑起来就行”的 Demo,而是一份从数据库建模到页面跳转、从余额校验到导出 Excel 全链路可追溯的完整财务业务闭环。如果你正卡在“功能实现了但讲不清设计依据”“老师问‘为什么这里不用 Filter 而用 FilterChain’答不上来”“答辩PPT全是截图没有架构图”,那这份带源码+数据库+完整文档的原生实现,就是你的后悔药。
2. 总体架构设计:三层结构与四个核心模块的职责边界
2.1 系统分层逻辑:为什么坚持“Servlet-Service-DAO”硬切分?
本系统严格遵循经典 Java Web 三层架构,但关键在于每一层的边界被显式约束,而非靠框架约定:
- Web 层(Servlet):仅处理 HTTP 协议相关事务——参数解析(含日期格式强校验)、会话管理(用户角色绑定到 request.getSession())、跳转控制(forward/redirect 明确区分读写操作)。绝不出现 SQL 字符串或金额计算逻辑。
- Service 层:承载全部业务规则。例如“凭证保存”需同时校验:① 科目是否启用、② 借贷方金额是否平衡、③ 操作人是否有该科目权限。这些规则全部封装在
VoucherService.saveVoucher()中,且方法内调用 DAO 时只传 POJO 对象,不传 request 参数。 - DAO 层:纯粹的数据存取。所有 SQL 语句写在
.properties文件中(如voucher.sql=INSERT INTO t_voucher (...) VALUES (?, ?, ?)),DAO 类通过ResourceBundle加载,避免 SQL 硬编码。Connection 管理由DBUtil统一封装,确保每次查询后连接归还。
提示:这种分层不是为了“看起来规范”,而是让答辩时你能指着代码说:“老师,这里校验借贷平衡在 Service 层,因为这是业务规则;而 Connection 关闭在 DBUtil,因为这是资源管理职责——如果混在一起,我无法向您证明这个校验逻辑是可靠的。”
2.2 四大核心模块划分:从业务流反推模块边界
系统按真实财务作业流程拆解为四个高内聚模块,每个模块对应独立的包结构和数据库前缀:
| 模块名称 | 包路径 | 核心表前缀 | 关键业务动作示例 |
|---|---|---|---|
| 基础档案管理 | cn.finance.module.base | t_base_ | 科目增删改查、部门/职员信息维护 |
| 凭证处理 | cn.finance.module.voucher | t_voucher_ | 凭证录入、审核、红冲、查询 |
| 账簿生成 | cn.finance.module.ledger | t_ledger_ | 总账、明细账、科目余额表动态生成 |
| 报表输出 | cn.finance.module.report | t_report_ | 资产负债表、利润表导出 Excel/PDF |
这种划分直接映射到菜单导航栏,学生做演示时可清晰说明:“点击‘凭证处理’进入凭证模块,其所有 Servlet 都在voucher包下,数据库操作只涉及t_voucher_*表——这保证了模块间低耦合。”
2.3 数据库设计原则:财务数据不可篡改性的落地实现
MySQL 设计严格遵循财务系统特性,非简单 CRUD:
凭证主表
t_voucher_header:CREATE TABLE t_voucher_header ( id BIGINT PRIMARY KEY AUTO_INCREMENT, voucher_no VARCHAR(20) NOT NULL UNIQUE COMMENT '凭证字号,格式:记-2024-0001', voucher_type TINYINT NOT NULL COMMENT '1=收款,2=付款,3=转账', voucher_date DATE NOT NULL COMMENT '凭证日期,影响账簿期间', status TINYINT DEFAULT 0 COMMENT '0=未审核,1=已审核,2=已红冲', created_by BIGINT NOT NULL, created_time DATETIME DEFAULT CURRENT_TIMESTAMP, -- 关键:添加审核人、审核时间字段,支持审计追溯 approved_by BIGINT NULL, approved_time DATETIME NULL, -- 物理删除标记,禁用 DELETE 语句 is_deleted TINYINT DEFAULT 0 );凭证分录表
t_voucher_detail:
使用debit_amount/credit_amount分离字段(非单个amount),强制借贷方向明确。
添加subject_code(科目编码)而非subject_id,避免因科目表变更导致历史凭证科目失效。科目表
t_base_subject:CREATE TABLE t_base_subject ( id BIGINT PRIMARY KEY, subject_code VARCHAR(10) NOT NULL COMMENT '科目编码,如:100101', subject_name VARCHAR(50) NOT NULL, parent_code VARCHAR(10) NULL COMMENT '上级科目编码,用于树形结构', level TINYINT NOT NULL COMMENT '科目级次:1=一级,2=二级...', is_enabled TINYINT DEFAULT 1 COMMENT '是否启用,停用科目禁止录入', is_leaf TINYINT DEFAULT 0 COMMENT '是否末级科目,末级才允许录入凭证' );注意:
subject_code采用定长数字编码(非自增 ID),确保科目体系稳定。所有凭证分录必须关联到is_leaf=1的末级科目,从源头杜绝“总账科目直接记账”的错误。
2.4 关键技术选型依据:为什么不用 Hibernate/MyBatis?
- Servlet 版本:使用
javax.servlet-api:4.0.1(兼容 Tomcat 9+),拒绝 Jakarta EE 命名空间迁移带来的兼容性风险,降低学生环境配置成本。 - JDBC 驱动:
mysql-connector-java:8.0.28,显式指定serverTimezone=GMT%2B8和useSSL=false,避免本地调试时因时区/SSL 导致连接失败。 - 前端技术:纯 JSP + JSTL + Bootstrap 4.6,无 JS 框架依赖。所有表格渲染用
<c:forEach>,表单提交用传统form action="xxx",确保学生能看懂每一行 HTML 如何与 Servlet 交互。 - Excel 导出:使用 Apache POI 4.1.2,不采用模板引擎,而是手写
XSSFWorkbook创建单元格样式(字体、边框、合并单元格),因为答辩时老师常问:“这个表头居中加粗是怎么实现的?”——你得能现场指出CellStyle.setAlignment(HorizontalAlignment.CENTER)这一行。
3. 核心模块实现详解:从凭证录入到报表生成的全链路代码实录
3.1 凭证录入模块:如何用原生 Servlet 实现“借贷必相等”强校验?
凭证录入页voucher_add.jsp提交到VoucherAddServlet,关键校验逻辑在VoucherService.validateAndSave()中:
// cn.finance.service.VoucherService.java public ResultVO saveVoucher(VoucherHeader header, List<VoucherDetail> details) { // 步骤1:基础校验(空值、日期有效性) if (details == null || details.isEmpty()) { return ResultVO.fail("至少需录入一条分录"); } if (!DateUtil.isValidVoucherDate(header.getVoucherDate())) { return ResultVO.fail("凭证日期不能晚于当前系统日期"); } // 步骤2:借贷平衡校验(核心!) BigDecimal debitSum = BigDecimal.ZERO; BigDecimal creditSum = BigDecimal.ZERO; for (VoucherDetail d : details) { // 强制金额精度为2位小数,避免浮点误差 d.setDebitAmount(d.getDebitAmount().setScale(2, RoundingMode.HALF_UP)); d.setCreditAmount(d.getCreditAmount().setScale(2, RoundingMode.HALF_UP)); debitSum = debitSum.add(d.getDebitAmount()); creditSum = creditSum.add(d.getCreditAmount()); } if (debitSum.compareTo(creditSum) != 0) { return ResultVO.fail(String.format("借贷不平!借方合计:%.2f,贷方合计:%.2f", debitSum.doubleValue(), creditSum.doubleValue())); } // 步骤3:科目有效性校验(查库确认是启用的末级科目) for (VoucherDetail d : details) { Subject subject = subjectDAO.findByCode(d.getSubjectCode()); if (subject == null || !subject.getIsEnabled() || !subject.getIsLeaf()) { return ResultVO.fail("科目[" + d.getSubjectCode() + "]不存在或未启用/非末级"); } } // 步骤4:保存(先头后明细,事务控制) try { conn.setAutoCommit(false); long headerId = voucherHeaderDAO.insert(header); for (VoucherDetail d : details) { d.setHeaderId(headerId); voucherDetailDAO.insert(d); } conn.commit(); return ResultVO.success("凭证保存成功,字号:" + header.getVoucherNo()); } catch (SQLException e) { conn.rollback(); log.error("凭证保存失败", e); return ResultVO.fail("系统异常,请重试"); } }设计说明:
- 所有金额运算用
BigDecimal并显式setScale(2, RoundingMode.HALF_UP),这是财务系统的铁律。若用double,0.1 + 0.2 != 0.3会直接导致校验失败。 - 借贷校验放在 Service 层最前端,不依赖数据库触发器——因为答辩时你要能解释“为什么不在数据库里做 check constraint?”,答案是:“触发器无法返回友好提示信息,且违反分层原则”。
- 事务手动控制(
setAutoCommit(false))而非依赖容器,让学生看清 commit/rollback 的实际位置。
3.2 账簿生成模块:如何用纯 SQL 动态生成科目余额表?
LedgerService.generateBalanceSheet()方法不依赖任何 ORM,直接拼接 SQL 查询:
// cn.finance.service.LedgerService.java public List<BalanceItem> generateBalanceSheet(String subjectCode, String startDate, String endDate) { // 构建动态SQL:查询指定科目及其所有下级科目的期初余额、本期发生额、期末余额 String sql = "SELECT " + " s.subject_code, s.subject_name, s.level, " + " COALESCE(SUM(CASE WHEN v.voucher_date < ? THEN d.debit_amount ELSE 0 END), 0) AS begin_debit, " + " COALESCE(SUM(CASE WHEN v.voucher_date < ? THEN d.credit_amount ELSE 0 END), 0) AS begin_credit, " + " COALESCE(SUM(CASE WHEN v.voucher_date BETWEEN ? AND ? THEN d.debit_amount ELSE 0 END), 0) AS period_debit, " + " COALESCE(SUM(CASE WHEN v.voucher_date BETWEEN ? AND ? THEN d.credit_amount ELSE 0 END), 0) AS period_credit " + "FROM t_base_subject s " + "LEFT JOIN t_voucher_detail d ON s.subject_code = d.subject_code " + "LEFT JOIN t_voucher_header v ON d.header_id = v.id AND v.status = 1 AND v.is_deleted = 0 " + "WHERE s.subject_code LIKE ? " + // 支持科目编码前缀匹配,如'1001%'查所有银行存款子科目 "GROUP BY s.subject_code, s.subject_name, s.level " + "ORDER BY s.subject_code"; // 执行查询(使用DBUtil获取Connection) List<BalanceItem> result = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, startDate); ps.setString(2, startDate); ps.setString(3, startDate); ps.setString(4, endDate); ps.setString(5, startDate); ps.setString(6, endDate); ps.setString(7, subjectCode + "%"); // 关键:动态匹配子科目 ResultSet rs = ps.executeQuery(); while (rs.next()) { BalanceItem item = new BalanceItem(); item.setSubjectCode(rs.getString("subject_code")); item.setSubjectName(rs.getString("subject_name")); item.setBeginDebit(rs.getBigDecimal("begin_debit")); item.setBeginCredit(rs.getBigDecimal("begin_credit")); item.setPeriodDebit(rs.getBigDecimal("period_debit")); item.setPeriodCredit(rs.getBigDecimal("period_credit")); // 计算期末余额(资产类:期初借+本期借-本期贷;负债类反之) item.setEndBalance(calculateEndBalance(item, rs.getShort("level"))); result.add(item); } } catch (SQLException e) { log.error("生成余额表失败", e); } return result; }参数说明与可修改点:
subjectCode传入"1001"即可查出所有银行存款相关科目(100101,100102...),这是财务系统多级科目查询的核心技巧。calculateEndBalance()方法根据科目级次(level)判断科目类型(一级科目编码规则隐含:1xxx=资产,2xxx=负债...),决定余额计算公式,避免在数据库里存科目类型字段,降低冗余。- 所有日期参数用
?占位符,防止 SQL 注入——这点在答辩中常被追问,你必须能指出ps.setString(1, startDate)这行的作用。
3.3 报表导出模块:手写 POI 生成资产负债表的细节把控
ReportExportServlet导出资产负债表时,对格式要求严苛:
// cn.finance.servlet.ReportExportServlet.java private void exportBalanceSheet(HttpServletResponse response) throws IOException { Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("资产负债表"); // 设置列宽(财务报表固定宽度) sheet.setColumnWidth(0, 5000); // 科目名称 sheet.setColumnWidth(1, 4000); // 期初余额 sheet.setColumnWidth(2, 4000); // 期末余额 // 创建标题行(合并单元格+居中加粗) Row titleRow = sheet.createRow(0); Cell titleCell = titleRow.createCell(0); titleCell.setCellValue("资产负债表"); titleCell.setCellStyle(createTitleStyle(workbook)); // 自定义样式方法 sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, 2)); // 创建表头行 Row headerRow = sheet.createRow(1); String[] headers = {"项目", "年初余额", "期末余额"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(createHeaderStyle(workbook)); } // 填充数据(从Service获取) List<BalanceItem> data = reportService.getBalanceSheetData(); int rowNum = 2; for (BalanceItem item : data) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(item.getSubjectName()); // 金额单元格:右对齐+千分位+两位小数 Cell debitCell = row.createCell(1); debitCell.setCellValue(item.getBeginBalance().doubleValue()); debitCell.setCellStyle(createAmountStyle(workbook)); Cell creditCell = row.createCell(2); creditCell.setCellValue(item.getEndBalance().doubleValue()); creditCell.setCellStyle(createAmountStyle(workbook)); } // 输出响应 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=balance_sheet.xlsx"); workbook.write(response.getOutputStream()); }关键样式实现createAmountStyle():
private CellStyle createAmountStyle(Workbook workbook) { CellStyle style = workbook.createCellStyle(); Font font = workbook.createFont(); font.setFontHeightInPoints((short) 10); style.setFont(font); style.setAlignment(HorizontalAlignment.RIGHT); // 右对齐 style.setDataFormat(workbook.createDataFormat().getFormat("#,##0.00")); // 千分位+两位小数 // 添加边框 style.setBorderTop(BorderStyle.THIN); style.setBorderBottom(BorderStyle.THIN); style.setBorderLeft(BorderStyle.THIN); style.setBorderRight(BorderStyle.THIN); return style; }提示:财务报表对数字格式零容忍。
#,##0.00格式确保1000000.00显示为1,000,000.00,而setCellValue(double)直接写数值,不拼字符串——这是专业性的体现。
4. 避坑指南:学生最容易翻车的5个致命细节
4.1 现象:登录后跳转到首页,但菜单栏不显示“凭证处理”等财务模块
原因:角色权限未正确绑定到 session。原生 Servlet 中,request.getSession().setAttribute("userRole", "accountant")后,在 JSP 中用<c:if test="${userRole == 'accountant'}">判断,但忘记在web.xml中配置 session 超时时间,导致开发时本地测试正常,部署到服务器后 session 过快失效。
解决:在web.xml中显式设置:
<session-config> <session-timeout>30</session-timeout> <!-- 单位:分钟 --> </session-config>并在登录成功后,立即将角色、用户名、ID 全部存入 session:
HttpSession session = request.getSession(); session.setAttribute("userId", user.getId()); session.setAttribute("userName", user.getName()); session.setAttribute("userRole", user.getRole()); // 角色必须是字符串,非数字编码4.2 现象:凭证录入时日期选择框选中“2024-03-15”,但保存后数据库存为“2024-03-14”
原因:MySQL 时区与 JVM 时区不一致。Tomcat 默认使用系统时区,而 MySQL 配置为SYSTEM,当服务器时区为 UTC 时,java.util.Date转java.sql.Date会丢失 8 小时。
解决:
- 在 JDBC URL 中强制指定时区:
jdbc:mysql://localhost:3306/finance?serverTimezone=GMT%2B8&useSSL=false - 在 Servlet 中解析日期时,用
LocalDate.parse()替代SimpleDateFormat:String dateStr = request.getParameter("voucherDate"); // 前端传"2024-03-15" LocalDate localDate = LocalDate.parse(dateStr); // 安全解析 java.sql.Date sqlDate = java.sql.Date.valueOf(localDate); // 无时区转换
4.3 现象:导出 Excel 时中文乱码,显示为“???”
原因:POI 4.x 版本对中文支持需显式设置字体,且Workbook必须用XSSFWorkbook(.xlsx),不能用HSSFWorkbook(.xls)。
解决:
- 创建字体时必须指定
setFontName("微软雅黑")或"SimSun":Font font = workbook.createFont(); font.setFontName("微软雅黑"); // 关键! font.setFontHeightInPoints((short) 10); - 响应头
Content-Type必须为application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,对应.xlsx。
4.4 现象:科目树形结构在页面上显示为扁平列表,无法展开收缩
原因:前端用<ul><li>渲染树,但未引入任何 JS 库,也未写递归 JSTL。学生常误以为c:forEach能自动处理父子关系。
解决:在BaseSubjectService中提供递归方法:
public List<Subject> buildSubjectTree(List<Subject> allSubjects) { Map<String, Subject> subjectMap = allSubjects.stream() .collect(Collectors.toMap(Subject::getSubjectCode, s -> s)); List<Subject> roots = allSubjects.stream() .filter(s -> s.getParentCode() == null || s.getParentCode().trim().isEmpty()) .collect(Collectors.toList()); roots.forEach(root -> buildChildren(root, subjectMap)); return roots; } private void buildChildren(Subject parent, Map<String, Subject> map) { List<Subject> children = map.values().stream() .filter(s -> Objects.equals(s.getParentCode(), parent.getSubjectCode())) .collect(Collectors.toList()); parent.setChildren(children); children.forEach(child -> buildChildren(child, map)); }JSP 中用 JSTL 递归标签(需自定义taglib)或改为简单层级渲染(最多三级,用c:if判断level)。
4.5 现象:审核凭证时,status字段更新为 1,但明细表t_voucher_detail中的approved_by未同步更新
原因:审核操作在VoucherApproveServlet中只更新了t_voucher_header表,遗漏了明细表关联字段。财务系统要求凭证头与明细状态严格一致。
解决:审核逻辑必须包含明细表更新:
// 在 VoucherService.approveVoucher() 中 String updateHeaderSql = "UPDATE t_voucher_header SET status = 1, approved_by = ?, approved_time = NOW() WHERE id = ?"; String updateDetailSql = "UPDATE t_voucher_detail SET approved_by = ?, approved_time = NOW() WHERE header_id = ?"; // 两个 SQL 在同一事务中执行5. 答辩验收实战:如何用3分钟讲清你的系统价值与设计深度
5.1 答辩演示的黄金三分钟结构
别一上来就说“我的系统有登录、凭证、报表”,评委听腻了。按这个节奏推进:
| 时间 | 动作 | 话术要点(自然说,不背稿) |
|---|---|---|
| 0-30秒 | 打开数据库,执行SELECT * FROM t_voucher_header LIMIT 1; | “老师请看,这张凭证的voucher_no是‘记-2024-0001’,编码规则我自定义了前缀+年份+序号,确保唯一且可读。” |
| 30-90秒 | 切换到VoucherService.java,定位validateAndSave()方法 | “这里我做了三重校验:第一,用BigDecimal强制两位小数,避免浮点误差;第二,借贷实时求和比对;第三,查库确认科目是启用的末级科目——这三点保证了凭证数据的法律效力。” |
| 90-180秒 | 运行导出功能,打开生成的 Excel,指向“期末余额”列 | “这个数字格式是 POI 手动设置的#,##0.00,右对齐,带边框。为什么不用模板?因为我要让您看到,连单元格样式都是我一行行代码控制的,这才是可控的财务系统。” |
提示:提前在 PPT 里放好三张图:数据库表结构截图(标出
is_deleted字段)、validateAndSave()方法关键代码截图(高亮setScale(2)和compareTo(0))、Excel 导出效果截图(箭头指金额格式)。演示时鼠标直接点 PPT 切换,不切 IDE。
5.2 评委高频问题应答清单(附真实血泪经验)
| 问题 | 应答策略(基于本方案真实设计) | 血泪经验 |
|---|---|---|
| “为什么不用 Spring Boot?显得技术陈旧。” | “老师,Spring Boot 的自动配置确实高效,但它隐藏了事务管理、连接池、MVC 流程等细节。而本系统用原生 Servlet,我能指着conn.commit()这行告诉您:凭证保存的原子性在这里保障;指着DBUtil.close(conn)说:连接泄漏风险在这里规避。” | 某同学答“因为简单”,被追问“简单在哪”,当场卡壳。要聚焦“可控性”和“教学性”。 |
| “科目编码用字符串,为什么不直接用主键 ID?” | “ID 是数据库内部标识,会变;科目编码是业务标识,必须稳定。比如科目‘100101’银行存款,即使未来调整科目体系,历史凭证里的subject_code仍能准确指向该科目,保证审计可追溯。” | 曾有学生用 ID,答辩时被问“如果删除了某个中间科目,历史凭证还能查到吗?”,答不上来。 |
| “凭证审核后还能修改吗?怎么防篡改?” | “审核后status=1,所有编辑接口会先查status,非 0 则拒绝。更重要的是,我用了物理删除标记is_deleted=0,而不是DELETE语句——所有凭证数据永久保留,符合《会计档案管理办法》。” | 某导师直接打开数据库,执行UPDATE t_voucher_header SET status=0 WHERE id=1;,然后问“这样是不是就能改了?”,学生没设校验,当场翻车。 |
| “报表数据实时性怎么保证?会不会缓存导致不准?” | “所有报表查询都走实时 SQL,不使用任何缓存。比如余额表,每次请求都执行SELECT ... FROM t_voucher_detail JOIN t_voucher_header,确保看到的是最新凭证数据。财务数据宁可慢一点,也不能错一点。” | 学生曾为“提升性能”加 Redis 缓存,被问“缓存失效策略是什么?如何保证缓存与数据库强一致?”,答不出。 |
5.3 文档与代码的验收检查表(答辩前必过一遍)
打印这份表,逐项打钩,能避开 90% 的低级失误:
| 检查项 | 检查方式 | 不合格表现 |
|---|---|---|
| 数据库脚本可重跑 | 删除库 → 执行finance_init.sql→ 检查t_base_subject是否有 20+ 条默认科目 | 执行报错,或科目数不足(少初始化数据) |
| 凭证编号自增 | 连续新增 3 张凭证,检查voucher_no是否为记-2024-0001、记-2024-0002… | 编号重复、跳号、或格式错误(如记-2024-1少前导零) |
| 借贷校验拦截 | 录入借方 100、贷方 99.99,提交后页面是否弹出“借贷不平”提示? | 页面空白、报 500 错误、或直接保存成功(校验失效) |
| 科目树形渲染 | 进入“基础档案-科目管理”,能否看到“资产类”→“货币资金”→“银行存款”三级结构? | 只显示一级,或点击展开无反应(JSTL 递归未生效) |
| 导出文件可打开 | 点击“导出资产负债表”,下载balance_sheet.xlsx,用 Excel 打开是否正常? | 打开提示“文件损坏”,或内容为空白(POI 版本与 JDK 不兼容,常见于 JDK17+ 用 POI 3.x) |
从那以后我每次帮学生过答辩,都强制他们用这个检查表走一遍——不是信不过代码,而是财务系统容不得“差不多”。哪怕一个setScale(2)漏了,都可能导致整张凭证被审计质疑。希望帮到你。
本文还有配套的精品资源,点击获取