ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java课设/毕设首选:基于Spring Boot的班费管理系统实战

Java课设/毕设首选:基于Spring Boot的班费管理系统实战 简介一份面向高校学生班费管理的Java Web设计源码适合Java学习者、毕业设计开发者及需要实现班级财务数字化管理的学生干部。项目完整覆盖班费管理、收支记录、报表生成等功能支持收入、支出分类记录与余额统计前端采用JavaScript、CSS、HTML实现交互与界面后端以Java为核心结合JSP、Servlet及Spring等框架思路并包含SQL数据库脚本。压缩包共342个文件其中Java源码与JSP页面构成后端主体JS与CSS负责前端交互和样式另有SQL脚本用于建表图片、字体及XML配置辅助项目运行整体大小9.32MB目录结构清晰便于按模块查阅。目前已有336人学习下载可作为课程设计或自学Java Web开发的实践参考。资源内提供需求分析文档、项目配置文件、前端组件与页面样式读者可据此理解班费管理系统的分层设计、表单交互与数据存储逻辑也可直接在此基础上二次开发。1. 高校班费管理这个题目为什么 Java 课设和毕设总选它班长在微信群里收班费红包和转账记录铺了一屏到学期末对账却少了 87 块谁也说不出钱花哪了学生想看支出明细生活委员只能把聊天记录截图往群里甩。“基于 Java 的高校学生班费管理系统设计源码”这个题目正好是把这一团乱麻变成结构化的账本学生查流水、班委记账、辅导员审批、每笔变动留痕天然覆盖了一个 Web 系统该有的所有要素——角色权限、状态机流转、事务一致性、统计报表。它在课程设计和毕业设计里被反复翻牌不是没道理的。适合正在选 Java 课设题目、需要应付答辩又不想堆砌框架的同学也适合想拿现成方案改造后快速交差的人。2. 技术选型与数据模型Spring Boot MyBatis MySQL 怎么分工2.1 为什么是 Spring Boot MyBatis而不是 SSH 或纯 JSP/Servlet班费管理系统本质上是一个典型的“围绕几张表做增删改查 状态流转”的业务系统业务规则不算复杂但涉及五六张表还有余额更新这种必须保证原子性的操作。用 Spring Boot 2.7.x 搭配 MyBatis-Plus 是当前最常见的课设组合Spring Boot 内嵌 Tomcat打好的 jar 包java -jar直接跑不需要再装容器、配 warMyBatis-Plus 把单表 CRUD 和分页查询封装好了我可以把主要精力放在“收支逻辑”而不是重复的 SQL 上。如果你是刚学 Java 的读者这个选型还有一个实际好处中间件少、配置短、报错案例多。相比 SSH 那一套 XML 配置Spring Boot 的自动装配省了太多事相比纯 JSP/Servlet你不用手动处理请求分发、参数绑定、事务管理那些东西除了让论文凑页数对这个项目本身的帮助不大。某些新版本 MyBatis-Plus 还提供了根据实体类生成建表 SQL 的 SchemaGenerator可以直接从 Java 类反向生成 CREATE TABLE 语句作为课设提效手段很顺手。但我一般不建议完全依赖它下面给出的手写 DDL 版本更能帮你在答辩时讲清楚每个字段为什么这么设计。2.2 角色与权限学生、班委、辅导员各能干什么班费管理最忌讳“人人可改账”所以第一件事是把角色立起来。常见做法是三种角色学生默认成员、班委生活委员或班长、辅导员或管理员用 user 表的 role 字段区分后端通过拦截器或过滤器按角色限制接口访问。这个模型往下延伸就是行级权限学生只能看自己班级的数据班级 ID 从登录用户上下文里取而不是让前端传。角色后端权限典型操作学生只读查看流水、查看收费项目进度、查看公示班委读写发起收费、登记收入、提交支出单、维护项目辅导员 / 管理员审批 监管审批或驳回支出单、查看名下所有班级账目很多课设源码在这一步偷懒把所有班级的数据都查出来再按角色过滤那不叫权限叫摆设。哪怕你当前不想接完整的登录框架也要在 Service 层保留“取当前用户班级 ID”的入口后面第 4 章的查询代码会强调这一点。2.3 六张核心表的字段设计与关系数据库命名为 class_fee一共六张表user、class_info、fee_item、income_record、expense_record、balance_log。前五张支撑业务balance_log 是贯穿全程的流水底账。user 表放登录账号、姓名、角色和所属班级class_info 表存放班级名称、专业、年级和当前班费余额fee_item 表记录一次收费项目比如“班服费 120 元/人”income_record 记录每个学生实际缴的钱expense_record 记录每一笔支出和审批状态。这里刻意把收入和支出拆成两张表而不是合并成一张“流水表”好处是两端都能独立核对收入对的是“应收多少人、实收多少人、还差多少人”支出对的是“谁申请的、谁批的、花在哪了”。balance_log 则是审计底线每一笔余额变动都写一条日志包含变动金额、变动后余额、收支方向、关联单据 ID。很多新手忽略了这张表等到答辩被问“你怎么证明余额是对的”就傻眼。class_info.balance 可能在并发下被覆盖流水表一旦写入就不改是对账时唯一可信的凭据。我解释一下 DECIMAL 的选择金额字段不要用 FLOAT 或 DOUBLE数据库里就用 DECIMAL(12,2)对应 Java 的 BigDecimal。12 位精度足够承载一个班级的班费规模2 位小数贴合人民币最小单位。余额可能短暂出现负数未通过审批前如果你的业务允许透支可以保留符号否则在审批逻辑里做余额校验后面会讲到。2.4 实体类与表的映射关系从 user 表看懂 MyBatis-Plus 注解表结构定完写实体类时要和表名、字段一一对上。以一个最简单的 user 实体为例Data TableName(user) public class User { TableId(type IdType.AUTO) private Long id; private String username; private String password; private String realName; private Integer role; private Long classId; TableLogic private Integer deleted; private LocalDateTime createTime; }TableName 声明的表名要和 DDL 一致TableId 标记主键并声明自增策略TableLogic 会让 MyBatis-Plus 在删除时自动转成 UPDATE deleted1而不是物理 DELETE这个设计能保住历史记录不被抹掉。createTime 不需要手动 set数据库 DEFAULT CURRENT_TIMESTAMP 已经兜底插入时 MyBatis-Plus 会把它当作普通字段处理如果没值也不影响入库。实体字段用驼峰数据库列用下划线对应关系靠全局配置自动映射这就是第 5 章要讲的 map-underscore-to-camel-case。3. 把工程搭起来pom、yml、建表脚本一次跑通3.1 用 Spring Initializr 生成工程并固定版本号搭工程最快的方式是在 IDE 里用 Spring Initializr 生成一个 Maven 项目Group 用 com.exampleArtifact 用 class-fee 或 classfee。Java 版本选 8 或 11Spring Boot 版本选 2.7.x。这个组合最稳原因在于 2.7.x 是社区文档和踩坑案例最多的版本Spring Boot 3.x 必须配 Java 17虽然更现代但部分老课设思路的兼容代码会出问题得不偿失。pom.xml 按下面这套最小依赖补齐即可parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesmysql-connector-j 是 MySQL 官方 8.x 驱动的新坐标Spring Boot 2.7.18 的依赖管理里已经配好了版本不用自己写版本号。MyBatis-Plus 3.5.5 与 Spring Boot 2.7 的兼容性经过了大量项目验证。Lombok 用来省掉 getter/setter如果你不习惯编译期注解也可以手写不影响逻辑。3.2 application.yml数据源、时区与字符集配置文件建议写成 yml比 properties 可读性好。注意三件事连接串必须显式声明 UTF-8 编码和 serverTimezone否则中文乱码和时区报错会在你第一次启动时同时找上门。server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/class_fee?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0driver-class-name 用 com.mysql.cj.jdbc.Driver这是 MySQL 8.x 的标准驱动类老的 com.mysql.jdbc.Driver 是 5.x 时代的不少人在这里踩坑。allowPublicKeyRetrievaltrue 是因为 MySQL 8 默认的 caching_sha2_password 认证在部分 JDBC 版本下要求显式放开公钥检索本地开发加上能少一次报错。log-impl 配成 StdOutImpl控制台会直接打印 MyBatis 执行的 SQL调试接口和答辩演示都很有用。3.3 建表脚本把六张表一次建出来下面直接给完整 DDL。字符集选 utf8mb4 而不是 utf8因为 utf8 在 MySQL 里存不下 emoji 和生僻字虽然班费系统用不太到但这是好习惯。CREATE DATABASE IF NOT EXISTS class_fee DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE class_fee; CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT 登录密码, real_name VARCHAR(50) NOT NULL COMMENT 姓名, role TINYINT NOT NULL DEFAULT 1 COMMENT 角色 1学生 2班委 3辅导员, class_id BIGINT NOT NULL COMMENT 所属班级ID, deleted TINYINT NOT NULL DEFAULT 0 COMMENT 逻辑删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_class (class_id) ) ENGINEInnoDB COMMENT用户表; CREATE TABLE class_info ( id BIGINT AUTO_INCREMENT PRIMARY KEY, class_name VARCHAR(100) NOT NULL COMMENT 班级名称, grade VARCHAR(20) COMMENT 年级, major VARCHAR(100) COMMENT 专业, balance DECIMAL(12,2) NOT NULL DEFAULT 0 COMMENT 班级班费余额 ) ENGINEInnoDB COMMENT班级表; CREATE TABLE fee_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, class_id BIGINT NOT NULL COMMENT 班级ID, name VARCHAR(100) NOT NULL COMMENT 收费项目名, expected_amount DECIMAL(12,2) NOT NULL COMMENT 每人应收金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0收集中 1已结束 ) ENGINEInnoDB COMMENT收费项目表; CREATE TABLE income_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, class_id BIGINT NOT NULL, fee_item_id BIGINT NOT NULL COMMENT 收费项目ID, payer_id BIGINT NOT NULL COMMENT 缴费学生ID, amount DECIMAL(12,2) NOT NULL COMMENT 实缴金额, pay_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(255) COMMENT 备注 ) ENGINEInnoDB COMMENT收入记录表; CREATE TABLE expense_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, class_id BIGINT NOT NULL, title VARCHAR(100) NOT NULL COMMENT 支出标题, category VARCHAR(50) COMMENT 支出类别如物资/活动/打印, amount DECIMAL(12,2) NOT NULL, applicant_id BIGINT NOT NULL COMMENT 申请人ID, detail VARCHAR(500) COMMENT 明细说明, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1通过 2驳回, auditor_id BIGINT COMMENT 审批人ID, audit_time DATETIME COMMENT 审批时间, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT支出记录表; CREATE TABLE balance_log ( id BIGINT AUTO_INCREMENT PRIMARY KEY, class_id BIGINT NOT NULL, change_amount DECIMAL(12,2) NOT NULL COMMENT 变动金额正为收负为支, balance_after DECIMAL(12,2) NOT NULL COMMENT 变动后余额, direction TINYINT NOT NULL COMMENT 1收入 -1支出, ref_type VARCHAR(20) COMMENT 关联单据类型income/expense, ref_id BIGINT COMMENT 关联单据ID, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT余额流水表;每个字段加 COMMENT 是答辩加分的习惯也能减少后续写代码的理解成本。user 在 MySQL 里不是保留字但有些数据库工具会对它高亮你改成 sys_user 也完全可以。表与表之间用 class_id 关联不建物理外键原因有两点一是课设数据量小靠代码逻辑保证引用关系就够了二是物理外键在后续维护和删除测试数据时会碍事。真需要约束加普通索引即可比如 user 表上的 idx_class。3.4 手动插入测试数据并验证连库成功建完表先插一条班级和三条用户后面所有接口测试都基于这些数据。INSERT INTO class_info (class_name, grade, major, balance) VALUES (软件工程2201, 2022, 软件工程, 0); INSERT INTO user (username, password, real_name, role, class_id) VALUES (stu001, 123456, 张三, 1, 1), (life, 123456, 李四, 2, 1), (teacher, 123456, 王老师, 3, 1);密码这里直接存明文是因为课设通常不做登录深究真要上线必须用 BCrypt 加密这是原则问题。接着写一个极简启动类和健康检查接口把项目跑起来SpringBootApplication MapperScan(com.example.classfee.mapper) public class ClassFeeApplication { public static void main(String[] args) { SpringApplication.run(ClassFeeApplication.class, args); } }RestController RequestMapping(/api/health) public class HealthController { GetMapping public String ok() { return class-fee is running; } }MapperScan 是 MyBatis 与 Spring Boot 集成的关键不扫描的话所有 Mapper 接口会报“找不到 Bean”。启动成功后浏览器访问 http://localhost:8080/api/health能看到返回字符串说明 Spring 容器、内嵌 Tomcat、数据源配置三者全部就绪。这一步是后续开发的地基如果启动失败先去确认 JDK 装的是 8 或 11、MySQL 服务真的在运行不要急着改业务代码。4. 收支记账的三个核心方法收班费、审批支出、查流水4.1 收班费写收入记录的同时更新项目进度和班级余额收班费不是一个 INSERT 语句的事。一次完整缴费至少要做三步写一条收入记录累计该收费项目已收金额把班级余额加上去。这三步必须放在同一个事务里否则会出现“钱收了但余额没变”的脏数据。下面是核心服务方法Service RequiredArgsConstructor public class IncomeService { private final IncomeRecordMapper incomeRecordMapper; private final FeeItemMapper feeItemMapper; private final ClassInfoMapper classInfoMapper; Transactional(rollbackFor Exception.class) public void recordIncome(IncomeRequest request) { FeeItem feeItem feeItemMapper.selectById(request.getFeeItemId()); if (feeItem null || feeItem.getStatus() 1) { throw new BusinessException(收费项目不存在或已结束); } IncomeRecord record new IncomeRecord(); record.setClassId(feeItem.getClassId()); record.setFeeItemId(feeItem.getId()); record.setPayerId(request.getPayerId()); record.setAmount(request.getAmount()); incomeRecordMapper.insert(record); feeItemMapper.increaseCollected(feeItem.getId(), request.getAmount()); classInfoMapper.changeBalance(feeItem.getClassId(), request.getAmount()); } }Transactional 是整个方法的安全网任何一个写操作抛异常前两步都会一起回滚。这里的自定义 SQL 本质上是 UPDATE class_info SET balance balance #{delta} WHERE id #{classId}用数据库自增而不是先查出来再 set能在一定程度上规避并发覆盖。请求体至少包含 feeItemId、payerId、amountamount 类型必须是 BigDecimal不是 double。你还可以在插入前校验 amount 大于 0、payer 属于该班级这些都是生产级细节。4.2 支出审批状态机流转与余额校验支出单的命运用状态机表达最清楚。班委提交时 status0待审批辅导员审批时要么置为 1通过并扣减余额要么置为 2驳回但不影响余额。下面这个审批方法带了状态校验和余额校验Transactional(rollbackFor Exception.class) public void approveExpense(Long expenseId, Long auditorId, boolean approved) { ExpenseRecord expense expenseRecordMapper.selectById(expenseId); if (expense null || expense.getStatus() ! 0) { throw new BusinessException(支出单不存在或已被处理); } if (!approved) { expense.setStatus(2); expense.setAuditorId(auditorId); expense.setAuditTime(new Date()); expenseRecordMapper.updateById(expense); return; } ClassInfo classInfo classInfoMapper.selectById(expense.getClassId()); if (classInfo.getBalance().compareTo(expense.getAmount()) 0) { throw new BusinessException(班费余额不足无法审批通过); } expense.setStatus(1); expense.setAuditorId(auditorId); expense.setAuditTime(new Date()); expenseRecordMapper.updateById(expense); classInfoMapper.changeBalance(expense.getClassId(), expense.getAmount().negate()); }approvedfalse 时改状态并直接 return不动余额。approvedtrue 时先比余额compareTo 返回负数说明余额不够直接抛异常不让流程继续。顺序上先改单子状态、后扣余额中间有事务保护扣款失败会回滚到审批前。但这里有个并发隐患两个请求同时读到 status0都走到扣款分支余额会被扣两次。单机课设里可以用条件更新解决UPDATE expense_record SET status1 WHERE id#{id} AND status0判断影响行数为 0 就抛“重复审批”。这个写法比先查再改更接近生产标准答辩时值得主动讲出来。4.3 流水查询balance_log 是唯一的审计凭据余额虽然在 class_info.balance 里直接能读到但我们仍然坚持写 balance_log原因只有一个balance 字段可能被并发写穿可能被人工改库而流水一旦写入就不再修改是对账的最终依据。维护流水最常见的方式是在 changeBalance 方法里同步插入一条日志public void changeBalance(Long classId, BigDecimal delta) { ClassInfo info classInfoMapper.selectById(classId); BigDecimal newBalance info.getBalance().add(delta); info.setBalance(newBalance); classInfoMapper.updateById(info); BalanceLog log new BalanceLog(); log.setClassId(classId); log.setChangeAmount(delta); log.setBalanceAfter(newBalance); log.setDirection(delta.compareTo(BigDecimal.ZERO) 0 ? 1 : -1); log.setCreateTime(new Date()); balanceLogMapper.insert(log); }direction 用 1 表示收入、-1 表示支出后面的统计 SQL 可以直接 SUM。changeBalance 在收入和审批两个场景被调用所以它适合独立成方法。查流水最容易被写成大杂烩 SQL常见做法是按 class_id 加时间倒序查 balance_log作为列表数据源。月度收支统计用下面这条 SQLSELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(CASE WHEN direction 1 THEN change_amount ELSE 0 END) AS income_total, SUM(CASE WHEN direction -1 THEN change_amount ELSE 0 END) AS expense_total FROM balance_log WHERE class_id #{classId} GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESC;DATE_FORMAT 按年月聚合一次查询拿到某班每个月的收入、支出和结余。如果你要验证 balance_log 和 class_info.balance 是否一致还可以跑一条对账 SQL按 class_id 汇总流水表变动金额和班级表余额比对。在查询接口里class_id 应当从登录用户上下文取而不是由前端传参即使当前没做登录模块也要在接口上预留服务端判断位置的注释。4.4 提交支出单班委端的入口与参数校验提交是班委的操作审批是辅导员的操作两者同样重要。提交支出单时只做插入和状态置 0不改任何余额。典型实现如下Transactional(rollbackFor Exception.class) public Long submitExpense(ExpenseRequest request) { ExpenseRecord record new ExpenseRecord(); record.setClassId(request.getClassId()); record.setTitle(request.getTitle()); record.setCategory(request.getCategory()); record.setAmount(request.getAmount()); record.setDetail(request.getDetail()); record.setApplicantId(request.getApplicantId()); record.setStatus(0); expenseRecordMapper.insert(record); return record.getId(); }这里不能扣余额很多新手在提交时就动了钱这是逻辑错误支出单只是“申请”还没被批准余额不应该变化。等到审批通过那一刻再扣款状态机才成立。返回 record.getId() 是为了让前端拿到单号后立即跳转详情页。参数校验方面amount 必须大于 0title 不能为空detail 限制 500 字以内。我的习惯是 Controller 只做格式校验业务校验放 Service这样单元测试可以直接测 Service 而不是启动整个 Web 层。5. 避坑排错从 Java 环境配置到 MySQL 时区的五条血泪经验5.1 Spring Boot 启动失败端口被占与依赖版本错乱现象工程一运行就抛 “Port 8080 was already in use”或者 java.lang.ClassNotFoundException 指向某个第三方包。原因端口被另一个进程占用依赖版本冲突常见的是 MyBatis-Plus 与 Spring Boot 3.x 乱搭或者把 MySQL 5.x 驱动和 8.x 驱动同时引入 classpath。解决端口占用用 netstat -ano | findstr 8080Windows或 lsof -i:8080macOS/Linux找到占用进程直接杀掉。版本问题先把 pom.xml 收敛成 3.1 节那套组合不要在同一个工程里出现两个 mysql 驱动坐标。另外确认本机 JDK 是 8 或 11用 java -version 看一眼再启动项目能排除掉一批 Java 环境配置相关的报错。5.2 连不上 MySQL 8时区报错和差 8 小时其实是同一回事现象首次启动控制台出现 “The server time zone value ... is unrecognized or represents more than one time zone”或者项目能跑但月报统计按北京时间看总是差 8 小时23:50 的支出被记到第二天。原因MySQL 8 的默认时区在部分系统上不是标准时区名JDBC 解析不了连接串没指定 serverTimezone 时Java 应用和数据库各自用了一套时间基准。这两个症状本质是同一个配置缺失。解决连接串改成 jdbc:mysql://localhost:3306/class_fee?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue同时把 application.yml 里的 jackson.time-zone 设成 Asia/Shanghai。改完重启启动报错消失如果月报还跨天异常再检查服务器系统时区是否 UTC统一之后症状会一起解决。5.3 createTime 查出来是 nullMyBatis 驼峰映射没生效现象数据库里 create_time 有值实体对象里 createTime 永远为 null前端列表时间列空着。原因数据库列是下划线风格Java 实体的字段是驼峰风格MyBatis 默认不自动映射下划线到驼峰必须显式开启。解决在 application.yml 的 mybatis-plus.configuration 下加 map-underscore-to-camel-case: true。如果你用了手写 XML resultMap就要保证 column 和 property 成对映射。这个配置对原生 MyBatis 和 MyBatis-Plus 都生效因为 MyBatis-Plus 底层就是 MyBatis。另外实体里 LocalDateTime 不要和 java.util.Date 混用统一类型后序列化和比较逻辑才稳定。5.4 金额用 double 计算月底账对不上现象收入登记时 0.1 0.2 显示成 0.30000000000000004班级余额和流水总和差几分钱。原因double 是二进制浮点无法精确表示十进制小数。班费流水几百笔误差不会自动抵消只会越滚越乱。解决数据库用 DECIMAL(12,2)Java 字段全用 BigDecimal所有金额操作走 BigDecimal不要用 double 接收。还有一个隐藏点SUM(amount) 返回 DECIMALMyBatis 映射到 BigDecimal 没问题但如果你把 SUM 结果映射到 Double 属性精度风险会重新出现。建议金额字段一律包装成 BigDecimal 类型不用基本类型。5.5 重复审批同一张支出单余额被扣两次现象班委在手机端连点两次“通过”支出记录只有一条班级余额却少了双倍。原因接口没做幂等或状态校验。先 select 再 update 的写法在并发下出现覆盖两个线程都读到 status0都走扣款分支。解决把审批 SQL 改成条件更新UPDATE expense_record SET status1 WHERE id#{id} AND status0判断影响行数为 0 就抛“重复审批”。更工程化的方案是在 expense_record 表加 version 字段配合 MyBatis-Plus 的 Version 做乐观锁。课设阶段建议用条件更新代码直观答辩也能讲清原理。记住凡是“先查再改”的写操作在课设里可能没事在真实系统里都藏着并发风险。6. 收尾进阶导出班费月报并验证账没算错6.1 用 EasyExcel 导出月度收支表核心功能做完之后导出一个 Excel 月报是很好的加分项也是辅导员真实会用的功能。用 EasyExcel 比 Apache POI 原生 API 少写一半代码导出实体加一个注解就能完成列头映射Data public class MonthReportRow { ExcelProperty(月份) private String month; ExcelProperty(总收入) private BigDecimal incomeTotal; ExcelProperty(总支出) private BigDecimal expenseTotal; ExcelProperty(净结余) private BigDecimal net; }导出接口把 4.3 节那条聚合 SQL 的结果直接映射成 List再通过 EasyExcel 写回响应输出流。浏览器访问这个接口就能下载 Excel。月份、收入、支出、结余四列信息量已经足够撑起一次答辩演示。前提是 balance_log 数据完整否则报表怎么导都是错的。6.2 验证你的系统没有算错钱写一个最小的 Spring Boot 测试用事务回滚防止测试数据污染开发库SpringBootTest Transactional class IncomeServiceTest { Autowired private IncomeService incomeService; Autowired private ClassInfoMapper classInfoMapper; Test void recordIncomeShouldIncreaseBalance() { BigDecimal before classInfoMapper.selectById(1L).getBalance(); IncomeRequest request new IncomeRequest(); request.setFeeItemId(1L); request.setPayerId(1L); request.setAmount(new BigDecimal(120.00)); incomeService.recordIncome(request); BigDecimal after classInfoMapper.selectById(1L).getBalance(); assertEquals(before.add(new BigDecimal(120.00)), after); } }Transactional 在测试方法上会把操作包进事务测试结束自动回滚不污染数据库。跑通测试后再用 curl 做一次全链路自测提交支出单、审批通过、查余额、查流水顺序走一遍。我第一次做这个系统时没给收入方法加事务测试卡在“插入成功但余额没变”排查半天才发现是事务边界的问题。后来养成了一个习惯凡是单次操作要写两张以上表的方法第一行就放 Transactional这个习惯帮我避开了不少生产事故。班费系统不大但事务、状态机、审计流水这些思路在真实业务系统里完全通用把这些玩明白这个课设就不只是交差而是真能写进简历的东西。希望帮到你。本文还有配套的精品资源点击获取
返回列表