简介:基于Java语言、MyBatis框架与Swing界面工具包构建的班费管理系统,面向高校计算机相关专业的数据库大作业、课程设计与毕业设计场景,适用于在校学生、教师以及需要项目借鉴的初学者。压缩包内共包含八十八个文件,主要有三十六个Java源码文件、二十个XML配置信息、十四个JFD界面设计文件、十二张PNG系统运行截图以及两个SQL数据库脚本,另有Markdown说明文档与工程配置文件,压缩包整体体积仅一百五十六KB,内容紧凑,便于导入开发环境后快速查阅。系统代码已经过完整测试并获得九十四点五分评价,下载后通过SQL脚本即可初始化表结构与示例数据,直接运行体验班费收入、支出、查询与统计等完整管理流程,也可根据需求在此基础上调整界面,增删收支分类、成员信息或权限控制等模块。目前已有四百二十四人学习下载,对于需要一份可靠数据库作业蓝本,或者想通过实际案例学习MyBatis与Swing整合开发的读者,具有很好的参考意义。
1. 一个班费管理系统,为什么是数据库大作业的“标准答案”区间
期末数据库大作业拿到“基于Java+Swing+Mybatis的班费管理系统”这个题目,不少人的第一反应是:一个记账小工具而已,能有多难。真正动手后才会发现,班费管理恰恰踩在数据库大作业最舒服的位置上——它逼你画清楚“学生、缴费、支出、操作员”之间的关系,也逼你用窗口程序把这些关系串起来。这篇文章就按我交付这类项目时的顺序,从表结构讲到Swing界面,再把容易让演示翻车的细节标出来。适合已经会写Java基础语法、想第一次把Mybatis和Swing拼成一个完整系统的同学参考。
2. 从表结构到Mybatis映射:班费系统先把数据模型立住
班费系统的业务说起来很小:收钱、花钱、查余额。但如果一上来就写代码,表结构大概率会变成一张“大流水表”,把支出和收入混在一起,到后面想做月度统计、按学生查欠费,SQL就会写得像打补丁。我一般会先按“主数据+流水数据”的思路建四张表:t_operator 操作员表、t_student 学生表、t_income 收入流水表、t_expense 支出流水表。
2.1 账本需要哪几张表:新建数据库与建表SQL
先在MySQL里新建数据库。班级缴费场景约定用utf8mb4,避免中文和一些特殊符号在写入时被截断:
CREATE DATABASE IF NOT EXISTS class_fee DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE class_fee;注意如果本机MySQL是8.0以上,utf8mb4是默认字符集;如果升级自5.7的老库,要确认库级和表级字符集,否则后面Java程序写中文可能变成乱码。下面建四张表,关键点我都写在字段注释里:
DROP TABLE IF EXISTS t_expense; DROP TABLE IF EXISTS t_income; DROP TABLE IF EXISTS t_student; DROP TABLE IF EXISTS t_operator; CREATE TABLE t_operator ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '操作员ID', username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT '密码', real_name VARCHAR(32) COMMENT '真实姓名' ) ENGINE=InnoDB COMMENT='操作员表'; CREATE TABLE t_student ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID', student_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(32) NOT NULL COMMENT '姓名', class_name VARCHAR(50) COMMENT '班级', phone VARCHAR(15) COMMENT '联系电话' ) ENGINE=InnoDB COMMENT='学生表'; CREATE TABLE t_income ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '缴费流水ID', student_id INT NOT NULL COMMENT '学生ID', amount DECIMAL(10,2) NOT NULL COMMENT '缴费金额', income_date DATE NOT NULL COMMENT '缴费日期', purpose VARCHAR(100) COMMENT '费用用途', remark VARCHAR(255) COMMENT '备注', operator_id INT NOT NULL COMMENT '登记操作员ID', CONSTRAINT fk_income_student FOREIGN KEY (student_id) REFERENCES t_student(id), CONSTRAINT fk_income_operator FOREIGN KEY (operator_id) REFERENCES t_operator(id) ) ENGINE=InnoDB COMMENT='班费收入流水表'; CREATE TABLE t_expense ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '支出流水ID', item_name VARCHAR(64) NOT NULL COMMENT '支出项目', amount DECIMAL(10,2) NOT NULL COMMENT '支出金额', expense_date DATE NOT NULL COMMENT '支出日期', category VARCHAR(32) COMMENT '支出分类', remark VARCHAR(255) COMMENT '备注', operator_id INT NOT NULL COMMENT '登记操作员ID', CONSTRAINT fk_expense_operator FOREIGN KEY (operator_id) REFERENCES t_operator(id) ) ENGINE=InnoDB COMMENT='班费支出流水表';为什么把收支分成两张表而不是一张流水表?因为收入表和支出表的字段并不对称:收入要关联到具体学生,支出只需要记录项目和分类。拆开之后,“按学生统计已缴费用”和“按分类统计支出”都是清晰的单表聚合。两个表都带operator_id,答辩时可以讲“每一笔流水都能追溯到是谁登记的”。金额用DECIMAL(10,2)而不用float/double,避免浮点误差,这是一个面试八股文都常提的细节。
建表之后还要插入少量测试数据,否则程序跑起来列表是空的,看不出效果。INSERT顺序要注意外键依赖:先插t_operator和t_student,再插t_income:
INSERT INTO t_operator (username, password, real_name) VALUES ('admin', '123456', '管理员'); INSERT INTO t_student (student_no, name, class_name) VALUES ('20250101', '张明', '软件2301'), ('20250102', '李雪', '软件2301'); INSERT INTO t_income (student_id, amount, income_date, purpose, operator_id) VALUES (1, 100.00, '2024-03-01', '班费', 1), (2, 100.00, '2024-03-01', '班费', 1);这段脚本能跑通的前提是外键字段的id顺序和插入顺序一致。如果先插t_income再插t_student,MySQL会直接报外键约束失败。DROP表的顺序正好反过来:先删有外键的流水表,再删主数据表,这也是上面SQL脚本第一段这样写的理由。
2.2 用Mybatis写收支流水查询:动态SQL、#{}与类型映射
表建好之后,程序侧要用Mybatis。这次作业我把Mybatis作为持久层,SqlSession在每次窗口操作里按需打开。下面是一段含三个查询条件的IncomeMapper.xml,支持按学生姓名、费用用途、日期区间过滤:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.bandfee.mapper.IncomeMapper"> <resultMap id="IncomeWithStudentMap" type="com.bandfee.entity.Income"> <id property="id" column="id"/> <result property="amount" column="amount"/> <result property="incomeDate" column="income_date"/> <result property="purpose" column="purpose"/> <association property="student" javaType="com.bandfee.entity.Student"> <id property="id" column="student_id"/> <result property="studentNo" column="student_no"/> <result property="name" column="name"/> <result property="className" column="class_name"/> </association> </resultMap> <sql id="queryCondition"> <where> <if test="studentName != null and studentName != ''"> AND s.name LIKE CONCAT('%', #{studentName}, '%') </if> <if test="purpose != null and purpose != ''"> AND i.purpose = #{purpose} </if> <if test="startDate != null"> AND i.income_date >= #{startDate} </if> <if test="endDate != null"> AND i.income_date <= #{endDate} </if> </where> </sql> <select id="selectIncomeList" parameterType="map" resultMap="IncomeWithStudentMap"> SELECT i.id, i.amount, i.income_date, i.purpose, s.id AS student_id, s.student_no, s.name, s.class_name FROM t_income i LEFT JOIN t_student s ON i.student_id = s.id <include refid="queryCondition"/> ORDER BY i.income_date DESC, i.id DESC </select> <insert id="insertIncome" parameterType="com.bandfee.entity.Income"> INSERT INTO t_income (student_id, amount, income_date, purpose, remark, operator_id) VALUES (#{student.id}, #{amount}, #{incomeDate}, #{purpose}, #{remark}, #{operator.id}) </insert> </mapper>这段XML有四个地方要理解到位。第一,#{}是预编译占位符,Mybatis会把它翻译成?,再走JDBC的PreparedStatement;${}是字符串直接拼接。带用户输入的条件必须用#{},这是Mybatis面试题里的高频考点。这个项目里所有条件都用了#{ },只有ORDER BY字段名如果要做动态排序,才需要白名单校验后用${}。第二,动态SQL里的 标签在XML中写时会遇到>和<,直接写<会破坏XML结构,所以>=写成>=,<=写成<=。这是我第一次跑这个项目时踩过的坑。第三,resultMap里用association把t_income和t_student关联起来,让Java实体不必增加多余字段。第四,Java实体里的LocalDate、BigDecimal与数据库DATE、DECIMAL的转换,由Mybatis内置的TypeHandler处理,课程设计不需要写自定义TypeHandler,除非你把字段类型设计成了枚举或者JSON。
2.3 这个作业为什么用Mybatis而不是JDBC或Mybatis-Plus
有些同学会问:大作业而已,为什么不用Java基础课教过的JDBC,或者用更省事的Mybatis-Plus。我的选择是原生Mybatis,理由有三个。第一,JDBC写CRUD时getString/setString满天飞,一旦表结构变化要改几十处,而Mybatis把SQL集中到XML里,改起来有明确边界。第二,Mybatis的resultMap和动态SQL正好能覆盖班费系统把“学生信息”拼到“缴费流水”上的需求,讲PPT时可以说清楚“查询是多表join,结果用resultMap映射成对象”。第三,如果直接用Mybatis-Plus,单表CRUD确实快,但在“流水+学生”的join查询上,要么手写XML,要么用Wrapper,反而多绕一层,对课程设计来说并不划算。
顺带一提,Mybatis还有一级缓存和二级缓存。大作业默认会话级别缓存基本够用;二级缓存如果没搞懂失效场景,反而容易出现“查一次后改了数据,窗口不刷新”的怪现象。我的建议是:这个规模的项目不要开二级缓存,把精力放在缓存设计的概念解释上,答辩问起来更稳。
提示:Java实体里的LocalDate、BigDecimal与数据库的DATE、DECIMAL怎么互相转换,背后是Mybatis TypeHandler在工作。这个项目直接用内置映射即可,不必写自定义TypeHandler。
3. Swing界面与Mybatis对接:按“能交作业”的标准跑通桌面端
Swing这部分才是很多人卡住的地方:DAO层写好了,窗口也能弹出来,但点按钮时界面假死、数据不刷新。要避免这些问题,先要把项目骨架和线程关系理清楚。
3.1 项目骨架:Maven依赖与包结构
我用Maven管理依赖,好处是通过pom.xml就能把mysql驱动和mybatis版本固定住,换电脑重新导入也方便。关键依赖如下:
<dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>mysql驱动版本要和本机MySQL版本匹配。MySQL 8用8.0.x驱动,驱动类名是com.mysql.cj.jdbc.Driver;MySQL 5.7用5.1.49,驱动类名是com.mysql.jdbc.Driver。版本不匹配时,最常见的就是启动报ClassNotFoundException或者“Unable to load authentication plugin”。
包结构我一般这样分:
- com.bandfee.entity:Student、Income、Expense、Operator
- com.bandfee.mapper:四个Mapper接口
- com.bandfee.util:MyBatisUtil、SwingUtils
- com.bandfee.ui:LoginFrame、MainFrame、IncomePanel、ExpensePanel
Mybatis的全局配置文件放在classpath根目录,叫mybatis-config.xml,核心内容如下:
<configuration> <properties resource="db.properties"/> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/IncomeMapper.xml"/> <mapper resource="mapper/OperatorMapper.xml"/> <mapper resource="mapper/ExpenseMapper.xml"/> <mapper resource="mapper/StudentMapper.xml"/> </mappers> </configuration>db.properties内容是这样的:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/class_fee?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8 jdbc.username=root jdbc.password=123456mapUnderscoreToCamelCase这个设置很重要:数据库字段student_no能自动映射成实体属性studentNo,省掉一长串result标签。然后是MyBatisUtil工具类,所有窗口共用:
public class MyBatisUtil { private static SqlSessionFactory factory; static { try (InputStream in = Resources.getResourceAsStream("mybatis-config.xml")) { factory = new SqlSessionFactoryBuilder().build(in); } catch (IOException e) { throw new ExceptionInInitializerError(e); } } public static SqlSession openSession() { return factory.openSession(); } }这段代码在类加载时执行一次,对应Mybatis初始化流程里的核心动作:读全局配置、解析Mapper XML、构建SqlSessionFactory。如果XML解析失败,程序会在启动时直接抛异常,而不是等到点击按钮才报错,方便尽早发现配置问题。
3.2 登录窗口与主界面:事件监听里的一次查询
登录验证是这个系统里最简单也最能体现Dao层用法的一段代码。用户在Swing文本框中输入用户名和密码,点击登录按钮后,用一个独立方法查询t_operator表:
private void doLogin() { String username = usernameField.getText().trim(); String password = new String(passwordField.getPassword()); try (SqlSession session = MyBatisUtil.openSession()) { OperatorMapper mapper = session.getMapper(OperatorMapper.class); Operator operator = mapper.selectByUsernameAndPassword(username, password); if (operator != null) { JOptionPane.showMessageDialog(this, "登录成功:" + operator.getRealName()); new MainFrame(operator).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } }配套的OperatorMapper.xml里写一个简单的select:
<select id="selectByUsernameAndPassword" resultType="com.bandfee.entity.Operator"> SELECT id, username, real_name FROM t_operator WHERE username = #{username} AND password = #{password} </select>这里三个参数说明要记住:密码字段用getPassword()拿char[],避免String留在内存里,这是Java面试基础里偶尔会被问到的安全习惯,课程作业里演示够了;使用try-with-resources关闭SqlSession,防止连接泄漏,Mybatis的SqlSession实现了Closeable,代码块结束自动归还连接;查询方法名要和Mapper接口方法名、XML里的id保持一致,否则Mybatis运行时报Invalid bound statement。
登录成功后的MainFrame里,一般用JTabbedPane放两个面板:班费收入面板和班费支出面板。每个面板顶部放查询条件,中间放JTable,底部放“新增”“删除”“刷新”按钮。这样整个系统的功能边界一眼能看清。
3.3 JTable展示收支流水:刷新模型而不是重建窗体
主界面的核心是一个JTable,用来展示收支流水。很多同学卡在“第一次能查出数据,第二次点查询之后表格没变化”,原因是没有把JTable的TableModel做对。我的做法是这样:
private void refreshIncomeTable(Map<String, Object> condition) { SwingUtilities.invokeLater(() -> { try (SqlSession session = MyBatisUtil.openSession()) { IncomeMapper mapper = session.getMapper(IncomeMapper.class); List<Income> list = mapper.selectIncomeList(condition); DefaultTableModel model = (DefaultTableModel) incomeTable.getModel(); model.setRowCount(0); // 先清空旧行 for (Income income : list) { model.addRow(new Object[]{ income.getId(), income.getStudent().getName(), income.getStudent().getStudentNo(), income.getAmount(), income.getIncomeDate() }); } } catch (Exception e) { e.printStackTrace(); } }); }这里有两个地方容易出错。第一个是setRowCount(0),如果不先清空,再执行addRow时旧数据会继续留在表格里,看起来像“数据没有更新”。第二个是查询动作在后台线程执行后,必须通过SwingUtilities.invokeLater把界面刷新放回事件队列,后面避坑章节会专门讲。
创建表格时最好指定列名,而不是让模型自动生成:
String[] columns = {"ID", "学生姓名", "学号", "金额", "缴费日期"}; DefaultTableModel incomeModel = new DefaultTableModel(columns, 0); incomeTable = new JTable(incomeModel); incomeTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);把列名写死在模型里,再配合上面的refresh方法,整个表格数据流的边界就清楚了。查询条件统一用一个Map传进去,动态SQL里就能按条件拼出SQL。这里的“金额”列如果不做格式化,直接显示100.00没问题;如果后期要显示“已交/未交”,就需要在Service层把学生名单和缴费流水做一次汇总,这是另一个话题,但至少本文这套结构已经能支撑。
4. 班费管理系统的5个常见问题与避坑清单
这一章列出来的每一条,都是我在这类项目里亲眼见过或者自己踩过的。写出来,权当给正在做的同学一把后悔药。
4.1 中文乱码:SQL文件、Java文件和JDBC URL各管一段
现象:导入SQL数据库后,在Navicat里手工插中文没问题,但程序写进去的中文显示成问号,或者界面下拉框里显示乱码。
原因:中文乱码不是单一地方造成的。SQL文件本身的编码、Java源文件编码、JDBC连接参数里的characterEncoding、MySQL连接器两端的字符集,只要有一环不是utf8mb4/UTF-8,就会在某个环节变成问号。
解决:SQL文件保存成UTF-8编码再导入;IDEA里把File Encodings设为UTF-8;JDBC URL上带characterEncoding=utf8。在mybatis-config.xml里写URL时,&要转义:
<property name="url" value="jdbc:mysql://localhost:3306/class_fee?serverTimezone=Asia/Shanghai&useSSL=false&characterEncoding=utf8"/>注意不要在db.properties里也把&写成&,properties文件直接用&,只有XML里才需要转义。
4.2 SqlSession没有关闭,连接池被占满
现象:程序跑了几次查询后,再点“新增一笔班费”就弹异常,提示“Cannot get a connection, pool exhausted”,重启软件才好。
原因:这通常不是Mybatis本身的问题,而是某次查询或新增操作里SqlSession没有关闭。Mybatis默认的POOLED数据源会借出连接,连接没还回去,池子就会被慢慢耗尽。
解决:所有操作都用try (SqlSession session = MyBatisUtil.openSession()) { ... }这种写法,让会话在代码块结束后自动close。我的经验是:不要在某一个按钮的监听器里手动openSession又忘记写finally,一两个地方漏掉,短期测试看不出来,做录入演示时就会当场翻车。
4.3 点击查询按钮后界面卡死:Swing线程问题
现象:窗口能弹出来,但单击“查询流水”按钮后,整个窗口变成白屏或无响应,过几秒才恢复,严重时Windows提示“未响应”。
原因:Swing的界面更新必须在Event Dispatch Thread(EDT)上执行。如果把数据库查询这种耗时操作直接放在按钮监听器里,EDT被阻塞,重绘事件排不上队,界面就假死了。
解决:查询、插入这些数据库操作放到SwingWorker里做,把结果通过done()方法送回EDT更新表格:
new SwingWorker<List<Income>, Void>() { @Override protected List<Income> doInBackground() throws Exception { try (SqlSession session = MyBatisUtil.openSession()) { return session.getMapper(IncomeMapper.class).selectIncomeList(condition); } } @Override protected void done() { try { List<Income> list = get(); refreshTable(list); } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, "查询失败"); } } }.execute();这样查询在后台线程执行,表格刷新回到EDT,窗口不会假死。不要把所有逻辑都塞到SwingUtilities.invokeLater里,invokeLater只是“提交任务到EDT”,不能把耗时操作变快。
4.4 Mybatis XML里的特殊字符转义与代码高亮排查
现象:XML中像i.income_date >= #{startDate}这样的条件无法解析,启动时Mybatis报错“The content of elements must consist of well-formed character data or markup”。
原因:在Mybatis的Mapper XML里,<是XML标记的开始字符,直接写>=没问题,但写<=就会让XML解析器认为开始了新的标签。
解决:把比较符号写成实体:>=写>=,<=写<=。如果条件里包含大量复杂判断,可以用 把整段SQL包进去。我的习惯是:只要SQL片段里出现小于号,就用CDATA或实体,宁多勿少。如果用的是IDEA,记得给Mybatis XML文件配置SQL方言,语法高亮能帮你在运行前看到转义错误,省去反复启动项目排查的时间。
4.5 JTable模型不刷新:数据流的最后一公里
现象:第一次进入主界面能看到数据,但点“按姓名查询”后表格没有变化,数据库中明明有匹配记录。
原因:最普遍的是忘记了调用setRowCount(0),或者没有从DefaultTableModel更新数据;另一种是把JTable换成了new JTable(list)重建对象,但界面上的表格引用还是旧的。
解决:用同一个DefaultTableModel实例,先setRowCount(0)清空,再addRow填充。如果用了SwingWorker,切记done()里要拿到查询结果后再setRowCount,否则线程竞争也会让表格显示旧数据。这个坑用一次就记住了。
5. 把交付物做全:SQL脚本、示例图片与答辩演示节奏
大作业交付通常不是“贴一张运行截图”就完事,而是SQL脚本、示例图片、源码一起交。SQL脚本建议做成“完整可重跑”的版本:先DROP再CREATE,再插入少量测试数据。测试数据里一定要包含不同班级、不同日期、不同金额的流水,否则答辩时没法演示“按日期区间查询”。
示例图片至少要截四类画面:登录成功、主界面展示收支流水、新增一笔支出、查询条件生效后的结果。截图时把无关个人信息遮挡一下会更正式。截图不要马赛克糊成一团,数据库大作业老师最看重的是能不能从图里看到表结构和数据对应关系。
最后给一个“结余统计”SQL,答辩时在Navicat里跑一遍,效果比口头讲“能查能增”好得多:
SELECT (SELECT IFNULL(SUM(amount), 0) FROM t_income) AS total_income, (SELECT IFNULL(SUM(amount), 0) FROM t_expense) AS total_expense, (SELECT IFNULL(SUM(amount), 0) FROM t_income) - (SELECT IFNULL(SUM(amount), 0) FROM t_expense) AS balance;这段SQL用两次标量子查询分别汇总收入和支出,再相减得余额,适合在答辩时演示“系统能对账”的能力。如果想看月度趋势,可以改成按DATE_FORMAT分组,例如SELECT DATE_FORMAT(income_date, '%Y-%m') AS ym, SUM(amount) FROM t_income GROUP BY ym,这样每月的班费收入趋势也讲得出来。
答辩的演示路径我一般按三步走:先打开ER图或SQL脚本,讲清楚四张表以及一对多关系;再启动系统,主界面查一次全量流水;然后新增一条缴费或支出,回到列表看最新数据,最后跑上面的结余SQL。每一分钟都让老师看到“数据在流动”,而不是在窗口美化上纠缠。
我做这类课设的习惯是:在答辩前一晚把整个SQL脚本从头删库再重跑一遍,只跑一遍能过,第二天才不至于在老师面前翻车。环境差异带来的坑远比业务逻辑多。希望这个方案能帮到你,把班费管理系统稳稳跑通。
本文还有配套的精品资源,点击获取