news 2026/10/29 23:49:50

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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直接跑,不需要再装容器、配 war;MyBatis-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 deleted=1,而不是物理 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.example,Artifact 用 class-fee 或 classfee。Java 版本选 8 或 11,Spring Boot 版本选 2.7.x。这个组合最稳,原因在于 2.7.x 是社区文档和踩坑案例最多的版本;Spring Boot 3.x 必须配 Java 17,虽然更现代,但部分老课设思路的兼容代码会出问题,得不偿失。

pom.xml 按下面这套最小依赖补齐即可:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

mysql-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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true 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: 0

driver-class-name 用 com.mysql.cj.jdbc.Driver,这是 MySQL 8.x 的标准驱动类;老的 com.mysql.jdbc.Driver 是 5.x 时代的,不少人在这里踩坑。allowPublicKeyRetrieval=true 是因为 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) ) ENGINE=InnoDB 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 '班级班费余额' ) ENGINE=InnoDB 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已结束' ) ENGINE=InnoDB 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 '备注' ) ENGINE=InnoDB 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 ) ENGINE=InnoDB 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 ) ENGINE=InnoDB 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、amount,amount 类型必须是 BigDecimal,不是 double。你还可以在插入前校验 amount 大于 0、payer 属于该班级,这些都是生产级细节。

4.2 支出审批:状态机流转与余额校验

支出单的命运用状态机表达最清楚。班委提交时 status=0(待审批);辅导员审批时要么置为 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()); }

approved=false 时改状态并直接 return,不动余额。approved=true 时先比余额:compareTo 返回负数说明余额不够,直接抛异常,不让流程继续。顺序上先改单子状态、后扣余额,中间有事务保护,扣款失败会回滚到审批前。

但这里有个并发隐患:两个请求同时读到 status=0,都走到扣款分支,余额会被扣两次。单机课设里可以用条件更新解决:UPDATE expense_record SET status=1 WHERE id=#{id} AND status=0,判断影响行数为 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,作为列表数据源。月度收支统计用下面这条 SQL:

SELECT 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 必须大于 0,title 不能为空,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 8080(Windows)或 lsof -i:8080(macOS/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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true,同时把 application.yml 里的 jackson.time-zone 设成 Asia/Shanghai。改完重启,启动报错消失;如果月报还跨天异常,再检查服务器系统时区是否 UTC,统一之后症状会一起解决。

5.3 createTime 查出来是 null:MyBatis 驼峰映射没生效

现象:数据库里 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) 返回 DECIMAL,MyBatis 映射到 BigDecimal 没问题,但如果你把 SUM 结果映射到 Double 属性,精度风险会重新出现。建议金额字段一律包装成 BigDecimal 类型,不用基本类型。

5.5 重复审批同一张支出单,余额被扣两次

现象:班委在手机端连点两次“通过”,支出记录只有一条,班级余额却少了双倍。

原因:接口没做幂等或状态校验。先 select 再 update 的写法在并发下出现覆盖,两个线程都读到 status=0,都走扣款分支。

解决:把审批 SQL 改成条件更新:UPDATE expense_record SET status=1 WHERE id=#{id} AND status=0,判断影响行数,为 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,这个习惯帮我避开了不少生产事故。班费系统不大,但事务、状态机、审计流水这些思路在真实业务系统里完全通用,把这些玩明白,这个课设就不只是交差,而是真能写进简历的东西。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/29 23:49:05

IDEA免费试用30天

dea eval reset 会允许你试用30天IDEA点击&#xff0c;输入https://plugins.zhile.io&#xff1b;点击OK

作者头像 李华
网站建设 2026/10/25 5:30:13

openGym四大新手计划详解:PPL、上下肢、全身、5×5该怎么选

openGym四大新手计划详解&#xff1a;PPL、上下肢、全身、55该怎么选 【免费下载链接】openGym Self-hosted gym & body-weight tracker — plan routines, log workouts (supersets, warm-ups, cardio), see which muscles are trained, fatigued or detrained, import fr…

作者头像 李华
网站建设 2026/10/23 12:59:42

Python `with` 语句和上下文管理器,到底帮你干了什么?7 个误用场景

Python with 语句和上下文管理器&#xff0c;到底帮你干了什么&#xff1f;7 个误用场景 先给结论&#xff1a;with 语句的本质&#xff0c;是在代码块进入时自动调用 __enter__、退出时自动调用 __exit__&#xff0c;它解决的核心问题只有一个——不管中间是正常结束还是抛了异…

作者头像 李华