资讯动态

Java课程设计不踩坑:超市积分管理系统表结构、事务与答辩技巧

发布时间:2026/10/8 14:38:10 来源:尧图企业网站定制
简介基于Java的超市积分管理系统项目完整资源包面向Java Web学习者、毕业设计及课程设计人群能够帮助深入理解会员、商品、积分等核心业务的后端实现并掌握Web分层开发思路。资源融合MVC设计模式、Servlet与JSP动态页面、JDBC数据库访问、DAO数据访问层等关键技术涵盖从业务建模、数据库设计到数据持久化的完整链路。压缩包共5个文件包含源代码zip、数据库SQL脚本、项目报告doc与必要说明txt整体约18MB结构清晰便于按需取用。目前已有360人学习下载。其中项目报告系统梳理了背景、需求分析、系统设计、实现与测试过程SQL脚本可用于建库建表配合源码中的JDBC/DAO层快速复现数据交互完整Java源码支持直接部署运行、断点调试与二次改造界面截图则直观呈现了系统界面与实际运行效果。对想通过完整项目案例提升Java Web实战能力或需要可参考的课程设计/毕业设计范文的读者这是一个高价值资源包。1. 为什么说超市积分管理系统是Java课程设计里的“稳赢”选题大四下学期一开学很多人就开始焦虑课程设计选题。做图书管理系统太老套做电商系统又怕时间不够超市积分管理系统恰好卡在中间——业务逻辑贴近真实消费一笔积分加一分积分攒够了兑换一瓶饮料会员等级变了积分倍率跟着变。这套“消费-累计-兑换”的闭环把数据库的增删改查、事务控制、界面联动全跑了一遍答辩时有东西可讲代码量又不至于失控。这篇文章按我实际带人做这套系统的路子从表结构、后端事务到答辩演示的坑一条条拆开讲。2. 表结构先行把积分流水拆成四张核心表后续少改一半代码2.1 需求拆解最小闭环是“消费-累计-兑换”不要一上来就写代码。先拿一张纸把系统要干的活列成用例。超市积分管理系统的核心场景就四个办卡建档、消费结账、积分兑换、记录查询。办卡建档对应会员表的插入操作字段至少要有卡号、姓名、手机号、积分余额。消费结账是链路最长的收银员输入会员卡号系统查出会员信息录入消费金额按积分规则算出本次应得积分更新会员积分余额同时在积分流水表里留下一条“增加”记录。积分兑换是反向操作会员用积分换商品系统检查余额够不够、库存有没有扣减积分、扣减库存流水里记一条“扣减”。记录查询则是把两张流水表按会员、按时间拉出来供对账和答辩演示用。功能边界也值得在报告里写清楚。常见做法是不做在线支付、不做多级会员等级因为这两个点会把项目复杂度翻倍对课程设计来说性价比很低。把这个闭环跑通评分点已经齐了增删改查占了事务占了表关联占了连“参数怎么配”这种答辩追问也能接住。功能拆完再回头设计表结构就顺了。2.2 建表脚本member、product、consume_record、points_log四张表就够别贪多。会员表、商品表、消费记录表、积分流水表。其中积分流水表是整套系统的灵魂它记录每一次积分变动的来源和去向而不是只存一个“当前总积分”。有了流水才能回答答辩时必问的那句“你怎么证明积分计算是对的”我一般建议学员直接用 MySQL 8.0 的 utf8mb4 字符集一次性避免后面中文乱码的麻烦。CREATE DATABASE IF NOT EXISTS supermarket_points DEFAULT CHARACTER SET utf8mb4; USE supermarket_points; CREATE TABLE member ( member_id INT AUTO_INCREMENT PRIMARY KEY, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT 会员卡号, member_name VARCHAR(50) NOT NULL, phone VARCHAR(20), points INT NOT NULL DEFAULT 0 COMMENT 当前积分余额, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT会员表; CREATE TABLE product ( product_id INT AUTO_INCREMENT PRIMARY KEY, product_name VARCHAR(100) NOT NULL, price DECIMAL(10, 2) NOT NULL, stock INT NOT NULL DEFAULT 0, redeem_points INT NOT NULL DEFAULT 0 COMMENT 兑换所需积分 ) COMMENT商品表; CREATE TABLE consume_record ( record_id INT AUTO_INCREMENT PRIMARY KEY, member_id INT NOT NULL, product_id INT NOT NULL, amount DECIMAL(10, 2) NOT NULL COMMENT 消费金额, points_earned INT NOT NULL COMMENT 本次获得积分, consume_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_member_time (member_id, consume_time) ) COMMENT消费记录表; CREATE TABLE points_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, member_id INT NOT NULL, change_type VARCHAR(10) NOT NULL COMMENT EARN增加/REDEEM扣减, change_points INT NOT NULL COMMENT 变动积分正数为增加负数为扣减, ref_record_id INT NULL COMMENT 来源消费或兑换记录ID, log_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_member_log (member_id, log_time) ) COMMENT积分流水表;字段类型上有几个选择值得在报告里解释。积分类字段用 INT 而不是 VARCHAR因为后续要直接做算术运算金额字段用 DECIMAL(10,2) 而不是 DOUBLE因为浮点数在累加时会产生意想不到的误差——这是数据库课程里讲过、但课设里最常见的翻车细节。时间字段统一用 DATETIME比 TIMESTAMP 少一个时区转换的坑后面第 5 章会专门说。索引不要乱加。这里选择在 member_id 时间字段上建联合索引是因为查询场景几乎都是“某个会员的流水按时间倒序”联合索引正好覆盖。非必要不加索引否则插入数据时索引维护会拖慢速度数据量小的时候感知不明显但答辩老师问“为什么这个索引有用”时这就是一个能加分的回答。至于外键我建议建表时先不写 FOREIGN KEY 约束而是在应用层用事务保证一致性。原因说来也现实课程设计里删除会员、删除商品的操作很常见外键约束会直接挡着不让删演示时非常尴尬。表结构里保留逻辑外键关系代码里用事务兜底比数据库层面硬约束好用得多。2.3 测试数据批量生成与两条防坑约定空表没法演示谁也不想在现场对着空白表格表演。测试数据要提前造好但不要只用 INSERT 一条条手敲。常见做法是写一段小脚本批量生成比如用存储过程循环插入 200 个会员、每人生成几笔消费流水。这样视听效果好积分余额也不会全部是 0。DELIMITER // CREATE PROCEDURE gen_test_data() BEGIN DECLARE i INT DEFAULT 1; WHILE i 200 DO INSERT INTO member(card_no, member_name, phone, points) VALUES (CONCAT(M, LPAD(i, 6, 0)), CONCAT(测试会员, i), CONCAT(138, LPAD(i, 8, 0)), FLOOR(RAND() * 500)); SET i i 1; END WHILE; END // DELIMITER ; CALL gen_test_data();LPAD 的作用是把数字补成固定位数让卡号看起来整齐比如M000001。RAND() 生成随机积分余额让数据分布更真实。这里有一个必须提前做的约定每笔消费后consume_record 和 points_log 必须成对出现。后面做积分对账时需要“流水总额”跟“会员积分余额”对得上我见过太多人只插消费记录、忘记插流水结果对账时一脸懵。另一条约定是 change_points 用正负数区分增加和扣减而不是靠 change_type 再做一次加减字段语义单一是减少 bug 的土办法但真的很管用。3. Java后端用JDBC连接MySQL积分增减必须放进同一个事务3.1 选型课程设计为什么优先JDBC而不是MyBatis后端连接数据库的选型常见就三条路原生 JDBC、MyBatis、Spring Boot MyBatis-Plus。很多学员一上来就选 Spring Boot觉得“企业都在用写了显得高级”。但你要想清楚一件事答辩老师第一眼看的不是技术栈新不新而是你能不能把这个技术栈讲透。Spring Boot 自动装配帮你省了配置可答辩追问“原理”时你只说得出“加了注解就能跑”场面就很尴尬。相反原生 JDBC 的每一行代码都是透明的Class.forName 加载驱动、DriverManager 拿连接、PreparedStatement 执行 SQL、ResultSet 取结果每一步都对应数据库访问的标准流程。这份流程背下来就是答辩现场最好的素材。所以我的结论是课程设计用原生 JDBC 打底报告里写明“为了深入理解数据库连接原理采用 JDBC 进行原生访问”——这句话既诚实又加分。如果你确实想体现一点现代感可以在报告附录提一句“该系统设计上可平滑迁移至 Spring Boot MyBatis”但主体代码不要乱。一个判断标准如果你的报告里描述的是“B/S 架构”用 Spring Boot 是合理的如果报告里写的是“C/S 架构”Swing JDBC 才是匹配的。技术栈要和架构描述一致这是很多学员栽跟头的地方。3.2 DAO层PreparedStatement查询会员与商品DAO 层的代码是整套系统的地基。拿“按卡号查会员”这个最简单的操作举例它是所有积分动作的入口——消费要查会员是否存在兑换要查余额够不够。public Member findMemberByCardNo(String cardNo) { String sql SELECT member_id, card_no, member_name, phone, points FROM member WHERE card_no ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, cardNo); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Member m new Member(); m.setMemberId(rs.getInt(member_id)); m.setCardNo(rs.getString(card_no)); m.setMemberName(rs.getString(member_name)); m.setPhone(rs.getString(phone)); m.setPoints(rs.getInt(points)); return m; } } } catch (SQLException e) { e.printStackTrace(); } return null; }这里的关键是 PreparedStatement 而不是 Statement。字符串拼接 SQL 在入门项目里很常见但一旦会员卡号里出现单引号要么报错要么被注入这是安全隐患。PreparedStatement 用占位符?绑定参数数据库会把它当纯数据而不是 SQL 片段处理代码也更干净。try-with-resources语法是我特别强调的Java 7 之后 Connection、PreparedStatement、ResultSet 只要写在 try 的括号里程序结束会自动关闭不用再手写 finally 关连接——这块偷懒就是第 5 章里“连接泄漏”的根源。DBUtil.getConnection() 是工具类内部维护一个 ThreadLocal 。简单场景直接每次 new 驱动实例也行但那会把连接管理散落得到处都是用一个静态工具类统一返回连接后续改连接池、改参数都只动一个文件。连接串里的参数也有讲究public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/supermarket_points ?useUnicodetruecharacterEncodingutf8 serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 123456; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }characterEncodingutf8是中文不乱码的前提serverTimezoneAsia/Shanghai让时间字段不要整个偏移 8 小时。很多人的代码在本地跑得挺好换台电脑就连不上或乱码九成是这两处没写。3.3 Service层积分累计与兑换扣减的“转账逻辑”积分累计的逻辑和银行转账是同构的一方积分加另一方流水表必须同步记录任何一步失败都要回滚。这就是为什么它必须放进同一个事务而不是调两个独立的方法。下面是核心代码片段的骨架。public boolean earnPoints(String cardNo, int consumeAmount) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 查会员 Member m memberDao.findMemberByCardNo(conn, cardNo); if (m null) { return false; } // 2. 计算本次积分假设 1 元 1 分可配置 int earned consumeAmount / 10; // 3. 更新会员积分余额 memberDao.updatePoints(conn, cardNo, earned); // 4. 插入消费记录 int recordId consumeDao.insert(conn, m.getMemberId(), earned); // 5. 插入积分流水 pointsLogDao.insert(conn, m.getMemberId(), EARN, earned, recordId); conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }从 setAutoCommit(false) 开始到 commit() 结束这一整段是一个原子操作。第 3 步更新余额成功后第 5 步插入流水如果失败conn.rollback() 会把第 3 步的更新也撤销会员积分不会凭空多出来。这个“全部成功才算成功有一步失败就全部撤销”的设计就是数据库事务的意义。更新积分时不能用SET points ?而要写成SET points points ?这是并发场景下的老生常谈。虽然课程设计没有高并发但同一个会员连续结账时points ?能保证读取-计算-写回整个过程不出偏差而且代码语义更清晰。兑换扣减的代码结构完全一样只是改成了points - ?再加一层“余额是否足够”的校验然后同步扣减商品库存。把“积分变动”和“流水记录”绑在同一个事务里这套系统最核心的业务规则就立住了。4. 界面与交互Swing还是浏览器取决于你答辩时敢不敢依赖Tomcat4.1 选型判断答辩演示场景下Swing更稳超市积分管理系统的界面选型在课程设计里无非两条路Java Swing 桌面程序或者 JSP/Servlet 网页程序。判断标准不是哪个更现代化而是你答辩现场的环境依赖。网上找的源代码五花八门有 Swing 的也有 JSP 的我见过不少人下载了一个 JSP 版本结果答辩现场 Tomcat 起不来直接表演技术翻车。Swing 的优势在于零部署编译出 class 或打成 jar 包双击就能跑只需要本机装了 JDK。而 JSP/Servlet 方案需要配置 Tomcat、打包 war 包、保证 8080 端口没被占用这一串步骤任何一个环节出问题演示就得中断。所以我的建议很明确课程设计选 Swing 做界面最稳报告里写“C/S 架构”目录结构清晰演示效果直接。如果题目明确要求 B/S 架构那就老老实实走 JSP Servlet但一定提前把 Tomcat 启动流程练熟最好导出 war 包而不是在 IDE 里点运行。4.2 用JTable把会员列表和数据刷新做出来Swing 里展示表格数据核心是 JTable 搭配 DefaultTableModel。新手最容易犯的错误是直接改 JTable 本身的数据其实表格的数据模型是 TableModelJTable 只是视图。会员列表加载的代码骨架很固定private void loadMemberTable() { ListMember list memberDao.findAll(); DefaultTableModel model (DefaultTableModel) memberTable.getModel(); model.setRowCount(0); // 先清空旧数据否则刷新会越加越多 for (Member m : list) { model.addRow(new Object[]{ m.getCardNo(), m.getMemberName(), m.getPhone(), m.getPoints() }); } }setRowCount(0)是刷新表格的关键不加这一行每次调用 loadMemberTable 都会在原有行后面追加数据点一次按钮表格多一倍行。这里先把列头在构造时定好String[] columns {卡号, 姓名, 手机号, 积分余额}; DefaultTableModel model new DefaultTableModel(columns, 0); memberTable new JTable(model); model.setRowCount(0);表格本身放在 JScrollPane 里否则表头不会显示。在 Swing 里所有数据加载操作最好放在 SwingUtilities.invokeLater() 里执行保证它跑在事件调度线程 EDT 上不然界面可能在多线程操作下卡死——这个细节不一定会遇到但答到“Swing 是单线程模型”时能说出来就是加分项。4.3 积分兑换的交互弹窗确认、扣减积分与表格刷新联动兑换交互的代码比想象中简单双击或点击某行商品弹确认框确定后调 Service再刷新两个表格。难点不在代码而在多张表的联动刷新。兑换成功之后会员表格要刷新积分变了积分记录表格也要刷新多了一条扣减流水漏掉任何一个界面上看到的都是旧数据。int col memberTable.getSelectedRow(); if (col 0) { JOptionPane.showMessageDialog(frame, 请先选择要兑换的商品); return; } int productId (int) productTable.getValueAt(col, 0); String productName (String) productTable.getValueAt(col, 1); int pointsNeed (int) productTable.getValueAt(col, 4); int option JOptionPane.showConfirmDialog( frame, 确认用 pointsNeed 积分兑换 productName 吗, 兑换确认, JOptionPane.OK_CANCEL_OPTION); if (option JOptionPane.OK_OPTION) { boolean ok pointsService.redeem(M000001, productId); if (ok) { loadMemberTable(); // 积分余额变了刷新会员表 loadRecordTable(); // 流水多了记录刷新积分表 loadProductTable(); // 库存减了刷新商品表 JOptionPane.showMessageDialog(frame, 兑换成功); } else { JOptionPane.showMessageDialog(frame, 兑换失败积分不足或库存不够); } }这段代码体现了 MVC 的一点味道业务逻辑全在 service 层界面只负责调用和展示。很多人把 SQL 直接写在按钮事件里图省事但答辩老师一眼就能看出代码没有分层扣分很重。另外注意 JTable 的 getValueAt 是按列索引取值列顺序要和 DefaultTableModel 里 addRow 的顺序严格一致这里我会在顶部加注释标明每列的索引含义防止改列的时候索引对不上。5. 避坑自查积分系统最容易翻车的五个现场5.1 积分重复累计刷新一次翻一倍现象同一笔消费点一次“结账”按钮积分加了一次窗口刷新一下积分又加了一次。严重的时候会员积分余额跟消费流水完全对不上。原因按钮没有做防重处理或者 Service 层没有校验“这笔消费是否已经积过分”。最常见的是刷新动作重新调用了整套积分累计流程而不是只刷新界面数据。解决两个层面同时处理。界面层结账成功后立即使按钮变为不可用等业务处理完之后再恢复数据库层在 consume_record 上加一个业务唯一索引比如 (member_id, consume_time, amount)同一秒内相同金额的重复消费记录会被数据库直接拒绝这招比代码判断可靠得多。我自己的习惯是以数据库约束为兜底代码防重为体验。5.2 中文乱码连接串、页面编码与数据库字符集现象会员姓名、商品名称在界面上显示成问号或者一堆乱码。原因往往不是一个地方的问题而是三层里某个环节没设编码排查时只改一处不起效。原因第一层是 JDBC URL 少了characterEncodingutf8第二层是数据库或表的字符集不是 utf8mb4第三层如果是 JSP 版本页面文件本身的编码也不是 UTF-8。解决按顺序排查。先查建表语句确认DEFAULT CHARSETutf8mb4再查 JDBC 连接串补上useUnicodetruecharacterEncodingutf8最后如果是 JSP确认pageEncodingUTF-8。Swing 版本一般不涉及第三层。这三处都对了乱码基本绝迹。5.3 连接泄漏数据库连接没关闭演示到高潮时程序卡死现象程序跑十几分钟开始变慢点啥都没反应最后卡死。重启后又正常过一会儿又卡。原因代码里用Connection conn DBUtil.getConnection()获取连接但没有在 finally 里关闭。连接被程序占用却不还给数据库MySQL 默认的连接数上限是 151池子用光了新请求只能排队等着表现就是越来越卡。解决所有数据库操作统一改成 try-with-resources。另外一个排查命令是 MySQL 里执行SHOW PROCESSLIST;能看到大量 Sleep 状态的连接挂在数据库上那基本就是泄漏实锤了。如果项目用了连接池也要检查连接池的空闲回收时间而不是只靠程序手动 close。5.4 演示翻车MySQL没启动、3306被占用现象答辩现场双击运行程序直接报Communications link failure或者Access denied for user。前者是数据库没连上后者是账号密码不对但现场很多人都先怀疑自己代码写错了。原因最常见的不是代码问题而是 MySQL 服务没启动或者 3306 端口被本机另一个程序占了。还有一类是换电脑演示时另一台的 MySQL root 密码跟代码里写的不一致。解决演示前半小时先手动启动 MySQL 服务然后拿 SQL 客户端连一次确认能通。端口占用可以执行netstat -ano | findstr 3306找到占用进程后改 MySQL 端口或者在连接串里显式指定端口。密码不一致的问题我的教训是连接串里的账号密码写成启动参数或配置文件别硬编码在 DAO 里换环境只改配置。5.5 积分过期时间DATE、TIMESTAMP与Java Date的时区差异现象积分数据带过期时间程序里明明写的是当天数据库存进去却变成前一天。或者反过来页面上显示的日期比实际多了 8 小时。这个在课程设计里不常见但如果做了“积分有效期”功能十有八九会踩。原因MySQL 的 TIMESTAMP 类型存储时会做时区转换而 JDBC 连接串里没指定serverTimezoneJava 运行时区跟 MySQL 服务器时区不一致时就会出现整 8 小时的偏移。解决两个办法要么连接串里加serverTimezoneAsia/Shanghai要么把时间字段类型改成 DATETIME。DATETIME 不依赖时区存什么就是什么课程设计没有任何理由使用 TIMESTAMP。如果已经建完表可以用ALTER TABLE points_log MODIFY log_time DATETIME;修改表结构这个命令属于“mysql数据库修改结构”的常用操作数据库同步软件在这种场景下反而不如一条 ALTER 直接。6. 答辩前的加分技巧用一条SQL做积分对账外加演示数据一键复位6.1 积分对账SQL流水总额 vs 会员积分余额答辩时最怕老师问“你这个系统算得对吗”。空口解释没用直接上对账 SQL。所谓对账就是把积分流水表里每个会员“增加的总和减去扣减的总和”计算出来跟 member 表的积分余额比对两者一致说明系统逻辑闭环不一致说明哪里漏了账。这条 SQL 就是整个系统的“后悔药”平时测试也能用。SELECT m.card_no, m.points AS account_points, COALESCE(SUM(CASE WHEN pl.change_type EARN THEN pl.change_points ELSE 0 END), 0) - COALESCE(SUM(CASE WHEN pl.change_type REDEEM THEN pl.change_points ELSE 0 END), 0) AS calc_points FROM member m LEFT JOIN points_log pl ON pl.member_id m.member_id GROUP BY m.member_id, m.card_no, m.points HAVING account_points ! calc_points;执行后如果有任何一行返回说明某个会员的积分余额和流水对不上优先查是不是消费后没写流水或者兑换时扣了余额漏了流水。一条 SQL 就能定位所有数据问题这个技巧写进报告“系统测试”一节里答辩老师基本都会点头。我这里顺便说一句这条 SQL 用的 CASE WHEN 分组聚合是 SQL 面试里常见的基础题能讲清楚就说明你的增删改查功底不是背出来的。6.2 演示前15分钟数据复位脚本与准备清单演示最怕数据被调乱了积分乱扣、商品库存归零、会员余额变成负数。我吃过一次亏之后养成习惯准备一个 reset_demo.sql 脚本演示前 15 分钟一键把数据恢复到初始状态。USE supermarket_points; SET FOREIGN_KEY_CHECKS 0; TRUNCATE TABLE points_log; TRUNCATE TABLE consume_record; TRUNCATE TABLE product; TRUNCATE TABLE member; SET FOREIGN_KEY_CHECKS 1; -- 重新插入少量、整齐、够演示用的数据 INSERT INTO member(card_no, member_name, phone, points) VALUES (M000001, 张三, 13800000001, 300), (M000002, 李四, 13800000002, 150), (M000003, 王五, 13800000003, 80); INSERT INTO product(product_name, price, stock, redeem_points) VALUES (矿泉水, 2.00, 100, 20), (可乐, 3.50, 50, 35), (购物袋, 0.50, 200, 5);TRUNCATE 比 DELETE 快得多但会重置自增 ID所以复位后 ID 从 1 开始演示时不会出现怪异的数字。脚本里先关掉外键检查是为了防止 TRUNCATE 关联表时报错清空后马上恢复。演示数据别塞太多三个会员、三件商品足够走完一遍“消费-积分-兑换”流程多了反而让界面显得乱。再准备一份演示脚本第一步启动 MySQL第二步执行 reset_demo.sql第三步启动 Java 程序。每一步做完都肉眼确认一遍再进答辩教室。这两年我带课设见过太多人把精力花在炫技上反而在最基础的“数据库连不上”“数据对不上”上翻车。做课程设计的第一步永远是让系统在自己手里稳定跑通然后才轮到加分项。一份干净的数据、一条能自证正确的 SQL比二十页花哨的 PPT 更让人踏实。希望帮到你让这套系统安安静静地帮你把答辩这一关过掉。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑