ARTICLE DETAIL

资讯详情

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

基于Java的超市积分管理系统课设:事务与并发全解析

基于Java的超市积分管理系统课设:事务与并发全解析 简介一套基于Java技术的超市积分管理系统完整项目资料内附项目报告、答辩PPT、源代码与数据库面向Java Web课程设计、毕业设计及初入企业级开发的学习者可帮助解决项目选题难、系统不完整、无参考实现等问题。资源以MVC设计模式为主线串联Servlet/JSP动态页面、JDBC数据库访问、DAO封装等核心知识点完整展示从需求分析、数据库建模到前后端联调的实现过程。压缩包共5个文件约18.21MB主要含项目报告doc、数据库SQL脚本、源代码ZIP、项目截图ZIP及阅读说明txtSQL脚本可直接导入源代码便于复现调试报告和截图用于对照功能模块与界面效果。目前已有360人学习下载适合作为系统设计参考、代码实践与答辩准备材料。通过此压缩包可获得开发思路、可运行代码和演示素材是一份覆盖Java Web开发全流程的实用学习资料。1. 为什么是超市积分管理系统这个课设选题比想象中更值从一个真实场景说起课程设计选题里一眼望去都是“xx管理系统”很多人挑花了眼最终落在“基于java的超市积分管理系统”这一项多半是因为数据表只有三到四张、功能看着不复杂。但这个看似简单的题目恰好把 Java 课设最该练的几件事全部覆盖——JDBC 连接与事务、三层架构的职责划分、积分流水这类不可逆数据的落库逻辑。它不是那种“管理员登录后对一张表增删改查”的水项目积分扣减和并发兑换两个点足以让它跳脱出普通 CRUD 的范畴。这篇文章写给真要动手把它做出来并打算拿去答辩的同学从数据库建模、核心业务代码到常见翻车点和答辩材料一次讲完照着调就能跑通。2. 从表结构起步会员、流水、兑换三张表的设计与连接参数打开源代码包第一个要看的一定是数据库脚本和配置文件。这两年课设普遍用 MySQL手上的源码可能是 JSPServlet 的经典三层也可能是 Spring Boot 或 SSM 改造版不管哪种第一件事是把核心表建出来再确定 JDBC 连接参数。超市积分管理系统的最小数据模型只有三张主表加一张跨表流水会员资料、积分流水、兑换商品再加上兑换记录。表的角色决定了后续所有业务代码怎么写所以这一步宁慢勿快。2.1 三层架构怎么拆JSP、Servlet、Service 各自的边界很多同学拿到源代码第一反应是打开 JSP 直接改页面但页面底下的数据库操作往往没有分层。一个能低成本改需求的项目至少要把三层拆开JSP 只负责展示和收集表单Servlet 负责接参数、做基础校验、转发请求真正写增删改查逻辑的是 Service 与 DAO。比如“给会员充积分”这个动作JSP 页面上是一个输入框加一个提交按钮Servlet 里要到 request 里取会员编号和积分值调 MemberService.addPoints()再由 MemberDAO 执行 UPDATE。这样以后把页面换成 Vue或者把数据库从 MySQL 换成其他数据库动的只有 DAO 层不会把所有 JSP 翻个底朝天。如果项目包里给的是 SSMSpring SpringMVC MyBatis框架职责边界其实是同一套话Controller 对应 ServletService 层是唯一允许写 Transactional 的地方Mapper 替代 DAO。判断你手上这份源码分层是否合理有个笨办法随便打开一个积分扣减相关类如果里面同时出现了 request.getParameter() 和 Connection conn ...那就是没分干净的典型。这种结构在演示时看不出问题但答辩老师多问一句“兑换记录存到哪张表”代码往页面里堆的人就会卡住。提示如果压缩包里既有 JSP 版又有 Spring Boot 版优先读懂 JSP 版后面补项目报告和讲基础概念会更稳。2.2 建表 SQL会员、积分流水、商品兑换之间的约束关系积分系统的数据库设计有一个原则余额可冗余流水必须完整。会员表里的 points_balance 存当前总积分是给人看的冗余值真正算得清账的是积分流水表每一笔增加、扣减、兑换都要单独落一行。不能只改余额不写流水否则积分对不上账没有后悔药吃。会员表我一般这样建CREATE TABLE t_member ( id INT PRIMARY KEY AUTO_INCREMENT, member_no VARCHAR(32) NOT NULL UNIQUE COMMENT 会员编号业务主键, member_name VARCHAR(64) NOT NULL, phone VARCHAR(20) DEFAULT NULL, points_balance INT DEFAULT 0 COMMENT 当前积分余额冗余字段, grade_level INT DEFAULT 1 COMMENT 会员等级1普通 2银卡 3金卡, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;积分流水表是整套系统的账本CREATE TABLE t_points_log ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL COMMENT 关联 t_member.id, change_type VARCHAR(20) NOT NULL COMMENT INCOME消费返利 / RECHARGE手工充值 / PAY兑换扣减 / REFUND退货返还, change_points INT NOT NULL COMMENT 正数为增加负数为扣减, balance_after INT NOT NULL COMMENT 变动后余额快照, related_no VARCHAR(64) DEFAULT NULL COMMENT 关联订单号或兑换单号, remark VARCHAR(255) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_member_time (member_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有两个关键设计。先看 change_points 直接用正负号而不单独建一个“增加/减少”标志位这样以后统计某会员总变动值直接 SUM(change_points) 就能对账。再看 balance_after 必须记录变动后的余额快照一旦积分被异常扣减可以按时间倒查是哪一笔操作之后余额开始不对的。很多项目只记“变多少”、不记“变完剩多少”作业能跑通但论文这里没有内容可写因为不可审计。商品表和兑换记录表的关系是这样兑换记录表持有 goods_id 和 member_id 两个外键语义并在兑换时把当时消耗的积分值冗余存一份防止商品积分价后续被改导致历史记录变味。CREATE TABLE t_goods ( id INT PRIMARY KEY AUTO_INCREMENT, goods_name VARCHAR(128) NOT NULL, points_price INT NOT NULL COMMENT 兑换所需积分, stock INT NOT NULL DEFAULT 0, status TINYINT DEFAULT 1 COMMENT 1上架 0下架 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_exchange_record ( id INT PRIMARY KEY AUTO_INCREMENT, exchange_no VARCHAR(32) NOT NULL UNIQUE COMMENT 兑换单号业务防重, member_id INT NOT NULL, goods_id INT NOT NULL, points_cost INT NOT NULL COMMENT 兑换时实际扣掉的积分, quantity INT NOT NULL DEFAULT 1, status TINYINT DEFAULT 1 COMMENT 1已兑换 2已发货 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_member (member_id), KEY idx_goods (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;到这里你会发现就算只做课设这套库也不止“会员一张表”那么简单因为兑换记录天然带着交易属性。把兑换单号设成 UNIQUE、把积分扣减放进事务后面第 3 章的代码才有地方落地。2.3 数据库连接配置URL、连接池、驱动三处不能写错拿到源码包第一件事不是看业务类而是改配置文件。老式 JSPServlet 项目一般是一个 db.propertiesjdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket_points?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456最典型的坑是 MySQL 8.x 下仍然使用 com.mysql.jdbc.Driver 这个驱动类到了 8.0 之后官方驱动类改成了 com.mysql.cj.jdbc.Driver多了一个 cj。如果项目用老驱动 jar 连 MySQL 8就会出现间歇性握手失败反过来 MySQL 5.7 用新 cj 驱动也一样容易抛异常。URL 里面三个参数必须逐字对齐useUnicodetrue 保证写入中文不乱码useSSLfalse 避免本地调试时 SSL 握手拖慢首次查询serverTimezoneAsia/Shanghai 解决驱动读系统时区不一致导致的 8 小时偏移。连接对象建议直接用项目自带的连接池课设里最常见的是 Druid至少保留 initialSize 和 maxActive 两个参数。如果源代码里看到 DriverManager.getConnection() 的裸写能换就换第 4 章的隔夜断连问题一大半就出在这里。3. 积分业务核心代码落地事务、并发和登录态的正确写法积分管理系统表面是增删改查真正拉开差距的地方在于写操作前后的一致性。下面三个场景是从这个项目里抽出来的最小闭环照着补进你的 Service 层答辩时就能讲清楚“事务怎么保证数据一致性”。3.1 登录鉴权所有积分写操作前先过 Session 这一关积分管理系统里最容易被轻视的是登录态。答辩演示时有人直接点链接进列表页看起来一切正常但老师问一句“不登录能不能访问积分查询”就露馅了。源代码里至少要在积分兑换、充值、商品管理这几个 Servlet 上挂同一个鉴权逻辑最省事的写法是用一个 Filter 拦截后台路径WebFilter(/admin/*) public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpSession session request.getSession(false); Object loginAdmin session null ? null : session.getAttribute(loginAdmin); if (loginAdmin null) { ((HttpServletResponse) res).sendRedirect(request.getContextPath() /admin/login.jsp); return; } chain.doFilter(req, res); } }WebFilter 注解里的 /admin/* 是路径匹配所有管理后台相关 Servlet 都放到这个路径下过滤器自动接管。注意登录 Servlet 本身不能被它拦住要么单独放在 /login 路径要么在 doFilter 里放行 login.jsp 和静态资源。这里还有一个细节request.getSession(false) 的 false 参数代表“会话不存在时不要新建”如果漏掉 false一次未登录访问也会写入一个 Session 对象后台日志看起来像“登录过”排错时很误导人。3.2 积分扣减事务余额更新和流水写入必须绑定这是整个项目的技术核心。给会员加积分或扣积分时常见的错误写法是先 UPDATE t_member 改余额再 INSERT 一条流水如果第二步抛异常余额已经变了但流水查不到对账直接失败。正确做法是把两步包进同一个数据库事务。以扣减积分为例public boolean deductPoints(int memberId, int cost, String relatedNo, String remark) { String sqlUpdateBalance UPDATE t_member SET points_balance points_balance - ? WHERE id ? AND points_balance ?; String sqlInsertLog INSERT INTO t_points_log(member_id, change_type, change_points, balance_after, related_no, remark) SELECT id, PAY, ?, points_balance, ?, ? FROM t_member WHERE id ?; try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try { try (PreparedStatement ps1 conn.prepareStatement(sqlUpdateBalance)) { ps1.setInt(1, cost); ps1.setInt(2, memberId); ps1.setInt(3, cost); if (ps1.executeUpdate() 0) { throw new BusinessException(积分不足或会员不存在); } } try (PreparedStatement ps2 conn.prepareStatement(sqlInsertLog)) { ps2.setInt(1, -cost); ps2.setString(2, relatedNo); ps2.setString(3, remark); ps2.setInt(4, memberId); ps2.executeUpdate(); } conn.commit(); return true; } catch (Exception ex) { conn.rollback(); throw ex; } finally { conn.setAutoCommit(true); } } catch (Exception ex) { log.error(积分扣减失败, ex); return false; } }这段代码有三个参数细节要特意说明。第一UPDATE 语句的 WHERE 条件带上 points_balance ?这一步不是“用查询条件做口号”而是把积分校验和余额扣减合并成同一个原子操作比先 SELECT 再 UPDATE 的两步查询可靠也回答了 Java 面试里常问的“怎么用数据库约束保证数据一致性”。第二INSERT 流水不直接传余额数字而是用 SELECT ... FROM t_member WHERE id ? 在事务内读取最新余额这样并发场景下 balance_after 快照不至于读到旧值。第三扣减积分用 -cost 记入 change_points增加积分用 cost统一由 change_type 字段区分业务来源报表 SUM(change_points) 一次汇总即可。3.3 兑换并发两次点击与超卖问题怎么防积分兑换比普通扣积分更接近线上交易因为同一事务里要同时校验积分、扣减积分、扣减库存。如果不做控制两个浏览器同时对同一个会员、同一件商品发起兑换两个请求都通过了积分充足查询最后把积分扣成负数或者商品库存从 1 变成 -1。解决方案是在事务里把行锁和条件更新结合。核心流程是三步try (Connection conn dataSource.getConnection()) { conn.setAutoCommit(false); try { // 1. 锁住会员行避免两个事务同时读余额再各自扣减 String lockMember SELECT points_balance FROM t_member WHERE id ? FOR UPDATE; // 2. 锁住商品行库存扣减前先拿到行锁 String lockGoods SELECT stock FROM t_goods WHERE id ? AND status 1 FOR UPDATE; // 在同一个事务里执行这两条 SELECT FOR UPDATE再判断余额和库存 conn.commit(); } catch (Exception ex) { conn.rollback(); throw ex; } }FOR UPDATE 的含义是给选中的行加排他锁另一个事务执行同一条 SELECT ... FOR UPDATE 时会阻塞等待直到第一个事务提交或回滚。细节上要注意三点只有 InnoDB 引擎支持行级锁MyISAM 下 SELECT FOR UPDATE 会退化成锁全表FOR UPDATE 必须出现在事务里单独执行等于没锁锁住的查询条件要能命中索引否则可能锁范围扩大。第 4.4 节的“重复点击兑换按钮”也是由这一步兜底。3.4 会员等级与积分规则把参数从代码里挪进表里除了事务这个系统还有一个容易被忽略的业务点会员等级与积分倍率。普通会员消费 1 元积 1 分银卡 1.2 倍金卡 1.5 倍。如果这几个数字直接写在 Java 代码里后面调整倍率就要重新编译部署。更稳妥的做法是建一张积分规则表把倍率、门槛、有效期都存成配置代码里读取规则后计算本次应得积分。这样课设报告里的“模块设计”章节也能多一层内容不只是会员等级字段而是把“规则可配置”讲出来。4. 常见问题排查跑通之后最容易翻车的 5 个细节我前后带过几版这种课设源码大多数问题不在技术本身而在连接池、编码和重复提交这三类不起眼的点上。下面五条按出现频率排序每条按“现象 → 原因 → 解决”写直接对号入座排查即可。4.1 隔夜再访问就断连连接池探活参数没配现象答辩前一天配好环境演示当天打开系统第一次查询必报 CommunicationsException刷新一下又正常。 原因MySQL 的 wait_timeout 默认 8 小时连接池里的旧连接长时间空闲会被服务端断开而连接池没有及时发现依然把失效连接交给业务代码。如果用 DriverManager 裸连每次请求新建连接反而不容易遇到但并发一高又会报 Too many connections。 解决给连接池打开空闲探活。Druid 配置里加上三行并调整 URL 参数druid.testWhileIdletrue druid.timeBetweenEvictionRunsMillis60000 druid.validationQuerySELECT 1 jdbc.urljdbc:mysql://localhost:3306/supermarket_points?useSSLfalseserverTimezoneAsia/ShanghaitestWhileIdle 让连接池在空闲时定期发送探活语句timeBetweenEvictionRunsMillis 设为 60 秒这样 8 小时空闲到来之前旧连接就已经被重建不会把一根失效连接交到积分查询的 Servlet 里。4.2 中文乱码JSP、Servlet、JDBC 三层编码不一致现象录入会员姓名“张三”列表页显示“寮犱笁”或问号同一个项目在不同机器上表现还不一样。 原因乱码是三层编码不一致的连锁产物JSP 文件编译编码、HTTP 请求响应 charset、Servlet 输出格式、JDBC URL 里的 characterEncoding、数据库表默认字符集任何一层断掉中文就到不了数据库。 解决按顺序检查四处。JSP 第一行写死% page contentTypetext/html;charsetUTF-8 %数据库连接 URL 确保带 characterEncodingutf8建表 SQL 里使用 CHARSETutf8mb4最后在 Filter 里对每个请求统一设置编码WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); res.setCharacterEncoding(UTF-8); chain.doFilter(req, res); } }有了这个过滤器后POST 请求里的中文参数会在进入 Servlet 前被正确解码GET 请求的中文参数在 Tomcat 8 以上也按 UTF-8 处理乱码基本能根除。4.3 手机号和商品名当主键历史数据关联错乱现象兑换列表按照手机号去查历史记录会员改了手机号后积分明细全部对不上。 原因t_member 表的手机号、t_goods 表的商品名称被当成了关联键代码里 JOIN 和 WHERE 都基于这些业务字段。手机号可改、商品名可重名只要业务值一变历史流水全部错位。 解决坚持使用自增 id 作为代理主键member_no、phone、goods_name 只当展示字段所有关联查询一律用 member_id 和 goods_id。答辩老师如果问主键设计回答“代理主键保证不变性业务主键只做展示”这一句话就比背概念强。4.4 重复点击兑换按钮幂等约束与库存双扣现象用户双击“兑换”按钮日志里出现两笔兑换单积分多扣一倍库存也多减一件。 原因前端没有禁按钮后端兑换逻辑没有幂等控制。第一个请求还在处理第二个请求已经进入 Servlet两个都通过积分校验然后各自扣减。 解决三层可叠加。后端最有效的是给 t_exchange_record.exchange_no 加唯一索引前端提交时生成一次性请求号服务端 INSERT 遇到 DuplicateKeyException 就返回“处理中”。第二层在兑换前后端各做一次防重前端提交后立即把按钮置灰文案改成“提交中”。第三层如果有余力用 Redis 的 SETNX 记录短时间内不重复的兑换请求课设项目做到前两层已经足够。4.5 流水表越查越慢缺联合索引的慢查询现象会员积分明细页在数据量到几万行后开始卡顿数据库 CPU 明显升高。 原因t_points_log 表里只有主键索引查询条件 WHERE member_id ? ORDER BY create_time DESC 时MySQL 对 member_id 做全表扫描再对结果排序数据量越大越慢。 解决建一个联合索引覆盖查询和排序两个维度ALTER TABLE t_points_log ADD INDEX idx_member_time (member_id, create_time);建立后要用 EXPLAIN 验证是否走了索引EXPLAIN SELECT * FROM t_points_log WHERE member_id 1 AND create_time 2025-01-01 00:00:00 ORDER BY create_time DESC;执行计划里 type 出现 ref、key 显示 idx_member_time才算生效。还要避免在查询条件里写 DATE(create_time) 2025-01-01 这类写法函数套列会让索引失效改成范围查询更稳妥。5. 项目报告与答辩 PPT把代码工作量讲成设计亮点压缩包里带项目报告和答辩 PPT说明这套源码的交付不只要跑通还要能讲明白。课设评分里报告和答辩往往比代码本身更决定档次因为代码是否原创无法现场验证但设计思路是否清楚很容易看出来。5.1 项目报告怎么写技术路线、模块划分、测试数据三块不能空项目报告最容易犯的错误是把类名方法名罗列一遍当成架构设计。老师想看的不是“有什么类”而是“为什么这么设计”。技术路线一节建议按这个顺序描述编程语言选 Java 的原因跨平台、课设环境兼容、后续可迁移到 Spring Boot数据库选 MySQL 的理由体积小、事务支持完整、InnoDB 适合流水表架构采用 JSPServletDAO 三层讲清楚每一层的职责和调用顺序。不要把“三层架构”停留在四个字给出一张模块调用路径比如 JSP 表单 → Servlet 参数校验 → Service 事务控制 → DAO 操作 MySQL。模块划分上把系统按功能切成五块登录鉴权、会员资料管理、积分增扣、商品兑换、积分统计报表。每块写清楚对应的 Servlet、Service 和关键 SQL。测试数据部分是很多报告被扣分的地方至少要准备三组有对比意义的数据普通会员余额 500 想兑换 800 积分的商品验证积分不足时的拒绝提示金卡会员余额充足验证扣减、流水、库存同时变化库存为 0 的商品点击兑换验证下架或售罄边界。这三组数据分别对应兑换成功、余额不足、库存不足三种分支截图放进 PPT比贴十页代码有用得多。5.2 PPT 页面与现场演示路径一条主线走下去答辩 PPT 控制在 12 页左右演示环节 8 到 10 分钟。页面顺序不重要重要的是演示时不要跳来跳去。我习惯把每一页和现场动作绑定起来PPT 页面演示动作要突出的设计点系统架构图静态页直接讲分层架构和请求流向数据库 ER 图现场打开 Navicat 展示三张表关系积分流水表 balance_after 快照设计会员列表页新增一个测试会员代理主键与业务字段分离积分充值和扣减对测试会员加 200 积分再扣 50余额变化后打开流水表展示两行记录商品兑换页兑换一个库存为 1 的商品连续点两次第二次被拒绝展示兑换单唯一约束积分统计页按会员编号查本月流水展示 EXPLAIN 走联合索引的结果这张表的核心作用是让 PPT 每一页都有可验证的操作而不是对着截图念文字。演示时间有限把事务、索引、防重这三个点讲到已经比大多数人强。5.3 答辩高频问题事务、索引、并发各备一句能打的回答答辩现场最容易出现这三类问题提前备好答案能少卡壳。第一类是“积分扣减时数据库崩溃怎么办”回答要落到事务回滚余额更新和流水写入在同一个 Connection 里setAutoCommit(false) 后 commit任何一步异常都 rollback。第二类是“积分流水表为什么越来越大”回答要落到索引和归档索引让查询走 ref流水表本身是追加写的定期把三个月前的记录归档到历史表。第三类是“两个用户同时兑换最后一个库存怎么办”回答要落到 FOR UPDATE 行锁先锁会员行和商品行再判断余额和库存锁保证第二个事务必须等第一个事务结束。这三组回答都能直接指向你代码里的具体位置不需要背概念。6. 从课设到真实系统积分过期、幂等和缓存改造课设做完之后如果想让这份源码真正称得上“系统”有三个改造方向我最推荐。第一个是积分过期。真实的超市积分都有有效期t_member 里需要加一个 points_expire_time 字段每天跑一个定时任务把到期积分标记过期并从余额扣减同时往流水表写一条 EXPIRE 类型记录。这个改动会让你的数据模型从“只管余额”进化到“管理积分生命周期”。第二个是幂等。前面用过兑换单号唯一索引做防重更进一步的做法是给所有积分写操作加一个 request_id每次提交带着它数据库幂等表碰到重复 id 直接返回已处理。第三个是缓存。商品列表和会员积分余额都适合放 Redis热点商品不用每次打到 MySQL积分扣减前先查缓存扣减后再删缓存配合数据库事务能扛住演示环境之外的并发压力。我自己做完这套项目后的习惯是先跑通再故意把数据库断掉重启看看系统崩溃后积分数据能不能对上。能对上的项目答辩才有底气。这份源码的边界在哪里、事务有没有兜底、并发有没有锁把这些验证过你面对老师提问时就不会慌。说到底课设项目拼的不是功能多而是把每个写操作的一致性讲到底。希望帮到你。本文还有配套的精品资源点击获取
返回列表