ARTICLE DETAIL

资讯详情

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

超市积分管理系统:Java Web课程设计中的事务、幂等与并发实战

超市积分管理系统:Java Web课程设计中的事务、幂等与并发实战 简介一份基于 Java 的超市积分管理系统完整项目资源适合正在学习 Java Web 开发、需要完成课程设计或毕业设计的学生也适合想了解传统 Servlet/JSP 项目结构的初级开发者。系统围绕会员、商品、积分和消费记录等核心业务展开涵盖 MVC 设计模式、Servlet 与 JSP 分工、JDBC 数据库访问、DAO 数据访问层封装等关键知识点能够帮助读者把项目背景、需求分析、数据库设计、代码实现、测试演示到答辩汇报的完整流程串联起来。整个压缩包共 5 个文件大小 18.21MB主要文件类型包括源代码 zip、数据库 SQL 脚本、项目材料报告 doc 和说明 txt另有一份项目截图压缩包便于对照实际界面理解系统运行效果。已有 360 人学习下载。借助项目报告、数据库脚本和完整源码可以快速掌握会员积分规则设计、积分增减逻辑、会员信息管理与数据交互的实现思路项目截图和报告文档还能作为答辩展示、文档排版和功能演示的参考样板。1. 超市积分管理系统它不只是个增删改查的课设超市积分管理系统是Java Web课程设计里出现频率最高的一类题目常见形态就是“源代码数据库项目报告答辩PPT”打包的一个压缩包。很多同学拿到手就懵代码能跑通但老师说系统太单薄答辩时被问“积分过期怎么处理”“并发兑换会不会超卖”一下就卡壳。这个系统真正的价值不在CRUD而在于它背后那一套业务规则积分怎么来、怎么花、怎么防重复、怎么保证账目一致。适合正在做Java课程设计或毕业设计的人也适合想快速理解一个完整Java Web项目结构的入门开发者。把这一层想明白了这套源码你才算真的“拿到手”。2. 先拆压缩包里的技术选型Java Web分层与数据库设计拿到这种项目的第一个动作不是打开IDEA而是先看数据库脚本和项目结构判断它到底是老式的ServletJSP还是SpringSpringMVCMyBatisSSM还是Spring Boot。这不是为炫技而是决定你后面怎么改。我经手的课设包里Spring Boot版本越来越多但老项目还是以JSPServlet为主。站在答辩角度SSM或Spring Boot更好讲因为它把控制层、业务层、持久层分得清清楚楚回答“为什么分层”时不会冷场。如果你拿到的是JSPServlet版本我也建议你在报告里按分层思路来写ControllerServlet只管参数接收和转发Service管业务DAO管数据库别把自己绕进JSP里写SQL的老路。2.1 三层架构在积分系统里的具体边界代码包里的Java类一般会按com.xxx.controller、com.xxx.service、com.xxx.dao这样分核心是Service层。以“积分兑换”为例Controller只做两件事从request里拿memberId和productId调用service.exchange(memberId, productId)然后跳转到结果页。Service层要做的事情就多查商品、查会员、算积分是否够、扣积分、扣库存、写兑换记录。这些操作必须在一个事务里否则会出现“库存扣了但积分没扣”这种账对不上的情况。DAO层就是MyBatis的Mapper接口或JDBC的PreparedStatement专门拼SQL。理解了这个边界你拿到源代码后就能快速定位想改积分规则进Service想改表结构进Mapper和SQL脚本。如果你看到的是ServletJSP项目没有Service接口那就把Servlet直接调DAO的写法看成是Controller和Service混在一起。改的时候建议单独抽出Service类哪怕只是把DAO调用包一层也能让你的报告多一张结构图答辩时好讲很多。这不算大改造但能一下把“只会贴代码”的印象扭转成“有工程意识”。2.2 数据库表设计会员、商品、积分流水一张都不能少一个规范的超市积分系统至少要有四张表会员表member、商品表product、积分流水表points_record、兑换记录表exchange_record。member表保存会员的当前积分余额points_balance而points_record表记录每一笔积分变动正数增加、负数扣减。为什么要两张表分开因为直接改member表省事但一旦积分不对你没有任何依据去追查。流水表就是积分系统的“账本”既能对账又能做积分过期、积分来源统计。这也是答辩时老师最爱问的点你能答出来就比只会贴CRUD代码的强很多。下面是适用于MySQL的建表脚本也是这种项目里最常见的设计。CREATE TABLE member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, points_balance INT NOT NULL DEFAULT 0 ); CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, points_price INT NOT NULL DEFAULT 0, stock INT NOT NULL DEFAULT 0 ); CREATE TABLE points_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, change_type VARCHAR(20) NOT NULL COMMENT EARN或SPEND, change_points INT NOT NULL, biz_ref VARCHAR(64) COMMENT 业务单据号用于幂等, create_time DATETIME NOT NULL ); CREATE TABLE exchange_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, member_id BIGINT NOT NULL, product_id BIGINT NOT NULL, points_cost INT NOT NULL, exchange_time DATETIME NOT NULL, status TINYINT DEFAULT 1 );这段建表SQL的逻辑要点是member表不直接存流水只存当前余额points_record表用biz_ref字段记录业务单据号比如订单号用来防止同一笔消费被重复累积积分exchange_record表记录每一次兑换状态字段方便做取消和退货。四张表以member_id和product_id关联外键在代码层维护不强制建物理外键这样导入数据库时不容易报错。参数的取舍在于points_balance用INT而不是DECIMAL因为积分通常是整数如果以后要做“积分抵现金”建议在member表加一个varchar的member_level字段但不要一开始就设计一堆用不上的字段课设阶段够用就好。这里还有个容易踩的坑很多人把积分余额和积分流水做在同一张表每天用一条update把累加后的值覆盖回去最后连他自己都分不清这个值是余额还是流水。正确的做法是member表只负责“当前余额”points_record只负责“发生过什么”。哪怕余额算错了你也能从流水表重算出来这就是审计思维生产系统里也这么设计。2.3 为什么建议你在报告里画ER图和分层图这部分不用代码但决定你报告和PPT的质量。很多源码包里的报告是自动生成的ER图画得乱七八糟。你要做的是打开数据库工具比如MySQL Workbench或Navicat自己重新画一张ER图member和points_record是一对多member和exchange_record是一对多product和exchange_record是一对多。这张图往报告和PPT里一放老师一眼就看出你懂关系。再画一张三层架构图浏览器-Servlet/Controller-Service-Mapper/DAO-MySQL标出每个层的关键类名比如MemberServiceImpl、MemberMapper答辩时就按这张图讲十分钟不冷场。记住报告和PPT不追求代码量追求的是“每一张图都能讲出设计理由”。很多代码包里的数据库文件是.sql脚本导入时我习惯用命令行直接执行而不是在Navicat里点来点去。下面这条命令写给你同样适合答辩时被问到部署细节mysql -uroot -p --default-character-setutf8 supermarket_points supermarket_points.sql如果导入时报“Unknown database”先执行CREATE DATABASE supermarket_points CHARACTER SET utf8mb4;再导入。这一句看起来简单却能救很多人一命——我见过有人因为没建库就直接导入折腾了两个小时以为代码有问题。3. 把积分规则写成代码累积、扣减、防重复一个都不能少系统核心不在登录注册而在积分的累积和兑换。我写一个最常见的积分累积Service层实现消费10元积1分订单号作为biz_ref做幂等。很多人会漏这一步导致刷新页面一次订单重复加分这是答辩现场最容易被当场演示出来的bug。3.1 累积积分事务里先查后写幂等是关键Service public class PointsService { Autowired private PointsRecordMapper pointsRecordMapper; Autowired private MemberMapper memberMapper; Transactional(rollbackFor Exception.class) public void earnPoints(Long memberId, String orderNo, int orderAmount) { // 1. 幂等校验同一订单号只能累积一次 int count pointsRecordMapper.countByBizRef(orderNo); if (count 0) { throw new DuplicatePointsException(订单已累积过积分); } // 2. 计算积分每10元积1分向下取整 int points orderAmount / 10; // 3. 写积分流水 PointsRecord record new PointsRecord(); record.setMemberId(memberId); record.setChangeType(EARN); record.setChangePoints(points); record.setBizRef(orderNo); pointsRecordMapper.insert(record); // 4. 更新会员积分余额 memberMapper.increasePoints(memberId, points); } }这段代码的逻辑说明先查询biz_ref是否已经存在防止前端重复提交或接口重放积分计算用orderAmount / 10整数除法天然向下取整先插入流水中再更新余额如果第4步失败事务会回滚第3步的流水也会消失。这里注意Transactional(rollbackFor Exception.class)不能省因为Spring默认只回滚RuntimeException如果业务异常是Exception的子类不加rollbackFor会导致事务不回滚库存和积分就永远对不上了。参数说明orderNo是外部传入的业务单号必须具备唯一性orderAmount是订单金额单位建议用“分”存储避免浮点数的精度问题。countByBizRef在Mapper里对应biz_ref字段最好建唯一索引否则高并发下幂等校验形同虚设。你可以在建表SQL里给biz_ref加UNIQUE KEY然后捕获DuplicateKeyException这样数据库层也能兜底防重比只靠Service更稳。3.2 积分兑换用乐观锁防超卖兑换和累积相反是扣减积分和扣减库存。最常见的翻车是会员积分余额为100同时发来两个兑换请求两个请求都读到余额是100结果都兑换成功余额变成负数。要解决这个问题不要在Service里用“先查询再update”的朴素写法而是用数据库行锁或乐观锁。这里我推荐乐观锁update语句带where版本号或余额下限。Transactional(rollbackFor Exception.class) public void exchange(Long memberId, Long productId) { Product product productMapper.selectByIdForUpdate(productId); Member member memberMapper.selectByIdForUpdate(memberId); if (product.getStock() 0) { throw new InsufficientStockException(库存不足); } if (member.getPointsBalance() product.getPointsPrice()) { throw new InsufficientPointsException(积分不足); } productMapper.decreaseStock(productId, 1); memberMapper.decreasePoints(memberId, product.getPointsPrice()); exchangeRecordMapper.insert(new ExchangeRecord(memberId, productId, product.getPointsPrice())); }用select ... for update是最直接的方式它在MySQL InnoDB下会对这行加写锁第二个请求必须等第一个提交或回滚所以并发下不会出现双重超卖。需要提醒的是selectByIdForUpdate必须放在事务内才有意义脱离了Transactional锁会在查询结束就释放。另外不要把锁加到整张表上比如“查库存总和”这种写法性能会很差。课设数据量小看不出问题但答辩时老师问“高并发怎么优化”你要能说出改成Redis预扣库存这一步在后面进阶部分展开。这里还有一个容易被忽略的点select ... for update只对InnoDB有效如果数据库表是MyISAM引擎它不生效。很多同学从老项目复制SQL出来表引擎还是MyISAM测试时怎么压测都超卖最后才发现是引擎问题。检查方法很简单SHOW TABLE STATUS LIKE product;看Engine列。3.3 积分扣减的SQL到底该怎么写直接修改余额的SQL最好加上条件比如扣减积分时update member set points_balance points_balance - #{points} where id #{id} and points_balance #{points}。这样的好处是数据库层面就能防止余额被扣成负数即使代码前一步漏了校验也不会产生脏数据。MyBatis的Mapper里这样写update iddecreasePoints update member set points_balance points_balance - #{points} where id #{memberId} and points_balance #{points} /update这里执行后要判断返回值如果影响行数为0说明余额不足Service层需要抛异常。这种写法称为条件更新比select for update更轻量很多生产环境也会用。要注意的是if判断和SQL条件要同时存在不要只靠代码里的if因为并发请求可能同时通过if校验。前面讲的select for update适合精确的“先查后做”场景条件更新适合保护单一字段的增减两种方案可以同时用但别叠太多锁否则系统会变慢。我一般倾向于简单的场景只放一个条件更新复杂的兑换场景加select for update不要为了炫技把每个方法都锁一遍。除了积分数值的更新兑换记录表也要在同一个事务里写入。很多人的代码里积分扣了、库存减了但exchange_record没插进去导致运营完全看不到谁兑换了什么。这类问题通常出现在“先更新后插入”的顺序上一旦插入失败前面两个更新已经提交账就平不了。有了Transactional任何一步抛异常都会全部回滚所以检查点就落在“事务注解是否真的生效”上尤其要注意同类内方法调用时Spring代理失效的问题。4. 项目报告和答辩PPT怎么把源代码讲成一套系统有了能跑的代码剩下的就是“项目报告答辩PPT”。很多人的代码是好的但报告写成流水账PPT贴满代码答辩时被问倒。这里我按自己整理这类项目的顺序一步一步说。4.1 报告结构从摘要到测试的七步写法一份合格的课设报告结构基本是固定的摘要、绪论、需求分析、系统设计、系统实现、系统测试、总结。不要直接交源码包里的旧报告而是照着自己代码改。摘要里必须写清楚“题目、关键技术、功能模块、测试结果”四件事别写“本系统功能强大”这种空话。需求分析部分用表格列功能需求会员注册登录、商品浏览、积分累积、积分兑换、兑换记录查询。系统设计部分放ER图和架构图。系统实现部分贴核心代码每段配2-3句说明比如为什么加biz_ref做幂等。系统测试部分做成表格测试用例、输入数据、预期结果、实际结果至少写6条。如果你不会写需求分析就打开你的数据库表从每个实体推出“谁在什么时候操作什么数据”。member表对应会员管理product对应商品管理points_record对应积分流水查询exchange_record对应兑换记录。这样写出来的需求一定和你的代码对得上不会出现报告里写了“积分过期提醒”而代码里却没有这个功能的情况。报告与代码不一致是明显的代写痕迹答辩老师一眼就能看出来。4.2 答辩PPT每页讲什么演示顺序决定你的分数答辩PPT不建议超过12页老师没时间看。我建议按这个顺序安排第一页题目和姓名第二页问题背景和意义第三页需求分析第四页系统架构第五页数据库设计第六页核心功能截图第七页核心代码第八页测试第九页总结与展望。每页只放一个重点讲的时候手要放在演示环境上不要照着PPT念。讲核心功能的时候先演示“消费订单产生积分”然后立刻查积分流水表证明记录有落库再演示“积分兑换商品”接着查exchange_record让老师看到数据一致性。这两步比说一百句话都有效。答辩时老师会看PPT里的架构图和ER图而不是看你贴的代码。所以PPT里的文字尽量少用图说话。架构图不要用Word画直接截图IDEA里的项目目录树再在关键类上标注释效果比手画的好。数据库部分就放ER图不要放一长串SQL。代码只挑一个核心方法比如earnPoints标出1、2、3的注释老师一看就知道有设计。4.3 把数据库脚本变成可视化图表很多答辩PPT里没有清晰的ER图这是一个很大的漏分点。你可以不装Visio直接用MySQL Workbench的Reverse Engineer功能反向生成ER图然后调整布局截图放进去。同样用Navicat的“模型”功能也能生成。操作路径是连接数据库 - 右键数据库名称 - 选择Reverse Database To ER Diagram生成后拖拽表位置导出为PNG。这样生成的图表包含字段名、主键、外键关系老师一看就知道你的是真系统。如果你手头只有源代码没有数据库文件先执行项目里的.sql脚本导入到本地MySQL再用上面的方式生成图表。这里额外提醒有些压缩包里的数据库文件是.sql而不是.dump导入时注意字符集用UTF-8。如果导入时报错先检查表结构里有没有中文字段注释再检查连接参数。数据库导入这件事建议你提前在答辩前一天的公共机房电脑上试一遍很多翻车都发生在教室机器没装MySQL或者密码不对。4.4 三分钟讲完整个系统的演示脚本答辩现场最容易犯的错是“只顾着点界面不讲设计”。我给自己准备的演示套路是前1分钟讲背景和需求中间1分钟讲架构和数据库设计最后1分钟现场演示“产生积分-查流水-积分兑换-查记录”的闭环剩30秒展示测试结果。这里给你一个可以直接套用的话术模板“本系统面向超市会员核心是保证积分账目的准确性。数据库四张表流水表负责追溯会员表负责余额。下面我演示一下这是一张100元的订单按10元积1分会员获得10分同时积分流水多了一条EARN记录然后用这10分兑换一个5分商品会员余额变成5库存减1兑换记录也生成了。整个流程在一个事务内完成任何一步失败都会回滚。”这段话讲完老师基本挑不出硬伤。5. 避坑指南从源码到跑通最常见的5个卡点前面讲的是“应该怎么做”这一章说“实际会怎么翻车”。以下5个问题是我在帮人调试这类系统时遇到频率最高的每个都按现象、原因、解决三个层次列清楚。5.1 启动项目后报数据库连接失败现象Tomcat起得来但一访问登录页就报Communications link failure或者Access denied。原因两个最常见——MySQL服务没启动或者连接配置里的用户名密码/URL时区不对。解决先确认MySQL进程在运行然后检查jdbc.properties里URL推荐写成jdbc:mysql://localhost:3306/supermarket_points?useSSLfalseserverTimezoneAsia/ShanghaiMySQL 8必须带serverTimezone否则会报时区错误。注意在XML里要写成amp;很多人死在这一步。我见过一个最隐身的问题代码里用的驱动类是com.mysql.jdbc.Driver而本地装的是MySQL 8驱动类找不到。MySQL 8以后驱动类改成com.mysql.cj.jdbc.Driver所以老项目里要同步改掉。如果项目里用的是Druid连接池直接删掉驱动注册那行让Druid自己去处理反而更省事。5.2 导入项目后找不到JDBC驱动类现象代码没有错误但运行到Class.forName(com.mysql.cj.jdbc.Driver)时报ClassNotFoundException。原因MySQL驱动jar没有打包进WEB-INF/libIDEA里也没加为Library。解决如果是Maven项目检查pom.xml里mysql-connector-java的scope别用provided如果是老项目直接把驱动jar放到Tomcat/lib或WEB-INF/lib。这里有个通用做法不要在代码里写Class.forName让Spring或Druid数据源去注册驱动你能少踩一个坑。检查依赖的时候顺便看一眼Java版本。有些代码包是用JDK 8写的如果你本机装的是JDK 11或17编译可能报错。最简单的办法是在IDEA里把Project Structure里的Project SDK和模块SDK都切成同一个版本再设置Maven的编译source/target为1.8避免低级错误。5.3 页面刷新一次积分重复累加现象用户支付后点刷新积分加了两次积分流水表出现两条相同biz_ref。原因前端没做提交按钮禁用后端也没做幂等校验刷新重新提交了订单号。解决这是第3章讲过的幂等后端必须查biz_ref。仅仅前端禁用按钮是不够的因为HTTP可能被重放。设置biz_ref时就用订单表的唯一订单号不要用自增ID否则换了环境就对不上了。这也是数据库设计时的关键字段。如果你拿到的源代码里没有biz_ref那这个系统大概率有隐患。你可以自己加一列再把原来的插入方法改成先查后插。这一步代码量不大但能在报告里写一条“避免重复积分”的亮点性价比很高。5.4 插入中文数据全是问号现象会员姓名、商品名称写入数据库后显示????或者乱码。原因数据库连接字符集、表字符集、JSP页面编码三处不一致。解决建库时强制CHARACTER SET utf8mb4连接URL加characterEncodingutf8Tomcat里如果用的是GET请求还要在server.xml的Connector上加URIEncodingUTF-8。逐层检查不要只改一处就以为完事。至于utf8和utf8mb4直接选utf8mb4因为超市商品名称里可能存Emoji。JSP页面本身也要保证文件保存编码是UTF-8IDEA右下角能看到。如果页面顶部只有pageEncodingUTF-8别忘了contentType里也写charsetUTF-8。这两行写错哪怕数据库全对前端看到的还是乱码。5.5 两个兑换请求并发库存扣成负数现象用脚本压测时库存为1的商品被两个人同时兑换成功库存变成-1。原因代码里先查库存再update两个请求都读到库存1判断都通过。解决像第3章那样用select ... for update或者条件更新。这里还要注意如果你的数据库存储引擎是MyISAMfor update不生效必须改成InnoDB。检查方法SHOW TABLE STATUS LIKE product;看Engine字段。如果你不愿意改表引擎可以用一条条件更新的SQL把库存扣减兜住比如update product set stock stock - 1 where id ? and stock 0影响行数为0就说明没抢到。这个方案比for update更简单但缺点是无法在同一个锁内读取商品其他字段。课设场景完全够用答辩时把两条路的取舍说清楚老师反而觉得你有判断力。6. 进阶从课程设计到能上线的积分系统还差哪几步代码包里的系统能跑但要真正上生产环境至少还差三件事缓存、异步、监控。先说缓存积分余额和商品库存都是读写频繁的数据用Redis缓存能避免每次请求都打MySQL。常见做法是在Redis里存会员积分扣减用Lua脚本保证原子性// 简化示例Redis扣减积分 String lua local c tonumber(redis.call(get, KEYS[1]) or 0) if c tonumber(ARGV[1]) then return -1 end redis.call(decrby, KEYS[1], ARGV[1]) return 1; Long result redisTemplate.execute( new DefaultRedisScript(lua, Long.class), Arrays.asList(member:points: memberId), points);这个脚本在Redis单线程模型下是原子操作不会出现超扣。但缓存和数据库之间要处理双写一致最简单的做法是扣减成功后异步发送MQ消息消费者更新数据库流水和余额。课设答辩如果能说出这一层老师会觉得你有工程意识。再说验证方法上线前除了功能测试一定要做并发验证。用JMeter开50个线程同时兑换同一商品观察最终的库存和积分流水是否一致。这一步能帮你发现所有“代码看着没问题”的隐藏bug。我当年就吃过亏功能测试全通过结果一压测就发现库存变成负数后来才意识到是表引擎的问题。从那以后我每次改完数据库脚本第一件事就是检查Engine和字符集。最后的教训是我自己做这类项目时留下的不要在报告里写“积分系统支持百万并发”但你代码里连个索引都没建。课设的评分标准是“设计合理、代码能跑、文档一致”不是“功能吹上天”。把biz_ref幂等、事务回滚、条件更新这几件事做扎实比加一堆花哨功能有用得多。希望帮到你。本文还有配套的精品资源点击获取
返回列表