资讯详情 Java Web积分系统实战:Servlet+JSP+MySQL全链路开发
📅 2026/10/18 4:51:02
简介本资源是一套完整的Java Web课程设计级项目——超市积分管理系统面向计算机专业本科生、Java初学者及Web开发入门者聚焦会员积分管理这一典型零售业务场景覆盖需求分析、系统设计到部署验证的全流程实践。压缩包共5个文件含项目报告.doc、答辩PPT位于.zip内、核心源码.zip、MySQL数据库脚本.sql及使用说明.txt总大小18.21MB其中报告详述MVC架构设计与DAO层实现SQL脚本支持快速建库建表源码基于ServletJSP开发体现JDBC数据库交互与前后端基础协同。已有360人学习下载读者可直接运行调试系统、对照报告理解模块划分逻辑、参考PPT梳理答辩要点并通过SQL脚本快速复现数据环境是掌握Java Web企业级小系统开发的高实用性学习材料。1. 一个超市收银台边的“积分黑匣子”它不只记分还能防刷、能预警、能导出Excel报表你有没有在超市结账时被店员问“会员卡带了吗今天积分翻倍。”——但后台系统却突然卡住积分没加、小票已打、顾客皱眉离开这个项目就是为解决这种“现场翻车”而生的它不是个玩具Demo而是一套跑在Tomcat上的真实Java Web系统从会员扫码、消费累加、积分兑换、等级升降到月底导出Excel报表给运营看全链路闭环。它用最朴素的ServletJSPMySQL组合没上Spring Boot没堆微服务但把DAO分层写得清清楚楚SQL预编译防注入积分变动留完整日志连“同一张卡30秒内重复扫码”都做了前端后端双校验。适合两类人一是刚学完JDBC和MVC想落地练手的学生代码结构干净、注释到位、数据库脚本直接可跑二是中小超市IT负责人拿来改改商户号、调调积分规则两周就能上线替代Excel手工记账。它不炫技但每行代码都在回答一个问题“顾客走后系统还记得清吗”2. 项目结构与技术栈解剖Servlet是心脏DAO是血管MySQL是骨血2.1 目录结构即设计思想src下四层拒绝“一锅炖”打开jf.zip解压后的源码目录你会看到典型的Java Web老派但稳健的分层结构src/ ├── dao/ // 数据访问层纯SQL操作无业务逻辑 │ ├── MemberDAO.java │ ├── PointRecordDAO.java │ └── GoodsDAO.java ├── model/ // 实体类与数据库表一一映射含getter/setter │ ├── Member.java │ ├── PointRecord.java │ └── Goods.java ├── servlet/ // 控制器层接收HTTP请求调用DAO跳转JSP │ ├── LoginServlet.java │ ├── AddPointServlet.java │ ├── ExchangeServlet.java │ └── ReportExportServlet.java ├── util/ // 工具类DBUtil封装JDBC连接、DateUtil、ExcelExportUtil │ ├── DBUtil.java │ └── ExcelExportUtil.java └── filter/ // 过滤器LoginFilter.java 拦截未登录访问提示这个结构刻意回避了“所有逻辑塞进Servlet”的新手陷阱。比如AddPointServlet只做三件事① 校验参数卡号非空、金额0② 调MemberDAO.updatePoints()和PointRecordDAO.insert()③ 转发到 success.jsp。业务规则如“满100元积1分”写在DAO方法内部而非Servlet里硬编码——这是后续改规则时唯一要动的地方。2.2 数据库设计5张表撑起全部业务主键外键全配齐jf.sql文件共建5张表核心是member会员、point_record积分流水、goods商品另加admin管理员和exchange_rule兑换规则。关键设计点如下member表中level字段为TINYINT值为1~5对应“普通→白银→黄金→铂金→钻石”等级不存字符串而是数字方便SQL排序和DAO计算point_record表设record_typeENUM(IN,OUT)区分“积分增加”和“积分扣除”避免用正负数混淆语义exchange_rule表含min_points最低兑换积分、goods_id可兑商品ID、status启用/禁用支持运营后台动态开关兑换项不用改代码。-- jf.sql 片段point_record 表定义含关键约束 CREATE TABLE point_record ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL, record_type ENUM(IN,OUT) NOT NULL, points INT NOT NULL DEFAULT 0, goods_id INT NULL, -- 兑换时填商品ID消费时为NULL remark VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (member_id) REFERENCES member(id) ON DELETE CASCADE );参数说明ON DELETE CASCADE是重点——当某会员被删除时其所有积分流水自动清除避免脏数据。很多学生项目漏掉这个导致“查不到会员却有几百条流水”的玄学问题。2.3 JDBC连接池DBUtil.java 里的“后悔药机制”util/DBUtil.java是整个项目的数据库命脉。它没用第三方连接池如Druid而是用java.sql.DriverManager手写了一个轻量级单例管理器但藏了两个关键细节连接超时自动重试getConnection()方法内嵌for (int i 0; i 3; i)循环每次失败后Thread.sleep(500)再试防止Tomcat启动时MySQL还没就绪导致应用报错退出PreparedStatement 预编译全覆盖所有DAO方法如MemberDAO.findByCardNo(String cardNo)均使用?占位符杜绝SQL注入。例如// dao/MemberDAO.java 片段 public Member findByCardNo(String cardNo) throws SQLException { String sql SELECT * FROM member WHERE card_no ? AND status ACTIVE; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, cardNo); // 关键参数化非字符串拼接 try (ResultSet rs ps.executeQuery()) { return rs.next() ? new Member(rs) : null; } } }逻辑说明try-with-resources确保Connection、PreparedStatement、ResultSet三层资源自动关闭哪怕中间抛异常也不泄漏连接。这是Java 7的标配写法但很多旧教程还在用finally{if(conn!null)conn.close()}极易漏关。3. 核心功能模块拆解从扫码到报表一个请求的完整生命周期3.1 会员扫码累积分AddPointServlet 的三次校验链当收银员扫会员卡触发/addPoint请求流程如下前端校验JSP中JavaScript检查卡号长度是否为12位、是否全数字Servlet校验AddPointServlet.javarequest.getParameter(cardNo)是否为空Double.parseDouble(request.getParameter(amount)) 0调用MemberDAO.findByCardNo(cardNo)查会员是否存在且状态为ACTIVEDAO层业务校验MemberDAO.updatePoints()查询当前积分SELECT points FROM member WHERE id ?计算新积分 原积分 Math.floor(amount / 100)满100元积1分插入流水记录INSERT INTO point_record (...) VALUES (?, IN, ?, NULL, 消费累积分)更新会员积分UPDATE member SET points ?, level ? WHERE id ?level根据points区间重算。// dao/MemberDAO.java 中 updatePoints 方法关键逻辑 public void updatePoints(int memberId, double amount) throws SQLException { int addPoints (int) Math.floor(amount / 100); // 向下取整 int currentPoints getCurrentPoints(memberId); // SELECT查询 int newPoints currentPoints addPoints; // 根据积分区间重算等级0-999:1, 1000-4999:2, 5000-9999:3, 10000:4 int newLevel 1; if (newPoints 10000) newLevel 4; else if (newPoints 5000) newLevel 3; else if (newPoints 1000) newLevel 2; String sql UPDATE member SET points ?, level ? WHERE id ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, newPoints); ps.setInt(2, newLevel); ps.setInt(3, memberId); ps.executeUpdate(); // 执行更新 } }参数说明Math.floor()确保“消费99.9元不积分”符合超市实际规则等级计算用if-else而非查表因规则简单且高频调用避免额外SQL开销。3.2 积分兑换商品ExchangeServlet 的事务一致性保障用户用积分换纸巾需同时完成① 扣减会员积分② 插入兑换流水③ 更新商品库存goods.stock减1。这三步必须原子执行否则出现“积分扣了但没给货”的资损。项目用JDBC手动事务实现// servlet/ExchangeServlet.java 片段 protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 int memberId Integer.parseInt(req.getParameter(memberId)); int goodsId Integer.parseInt(req.getParameter(goodsId)); // 1. 扣积分调用DAO memberDAO.deductPoints(conn, memberId, goodsPrice); // 传入conn复用同一连接 // 2. 插入兑换流水 recordDAO.insertExchangeRecord(conn, memberId, goodsId, goodsPrice); // 3. 扣库存 goodsDAO.reduceStock(conn, goodsId, 1); conn.commit(); // 全部成功才提交 req.setAttribute(msg, 兑换成功); } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { /* 忽略回滚异常 */ } } req.setAttribute(msg, 兑换失败 e.getMessage()); } finally { if (conn ! null) { try { conn.close(); } catch (SQLException e) { /* 忽略关闭异常 */ } } } req.getRequestDispatcher(result.jsp).forward(req, resp); }逻辑说明所有DAO方法都增加Connection conn参数如deductPoints(Connection conn, ...)确保三步操作在同一数据库连接上commit()或rollback()才有意义。这是手写事务的核心比SpringTransactional更底层也更易理解。3.3 月度Excel报表导出ReportExportServlet 的流式生成点击“导出本月报表”后端不生成文件再下载而是直接写响应流浏览器自动弹出保存框。util/ExcelExportUtil.java使用Apache POI 3.17项目lib目录下有poi-3.17.jar关键在内存控制// util/ExcelExportUtil.java 片段 public static void exportToResponse(HttpServletResponse response, ListPointRecord records) throws IOException { HSSFWorkbook workbook new HSSFWorkbook(); // 使用HSSF而非XSSF兼容老版Excel HSSFSheet sheet workbook.createSheet(积分流水); // 表头 HSSFRow header sheet.createRow(0); String[] titles {会员卡号, 姓名, 类型, 积分, 商品名, 时间}; for (int i 0; i titles.length; i) { header.createCell(i).setCellValue(titles[i]); } // 数据行注意records.size() 5000时会OOM项目默认限制查询最近30天 for (int i 0; i records.size(); i) { HSSFRow row sheet.createRow(i 1); PointRecord r records.get(i); row.createCell(0).setCellValue(r.getCardNo()); row.createCell(1).setCellValue(r.getMemberName()); row.createCell(2).setCellValue(r.getRecordType().equals(IN) ? 收入 : 支出); row.createCell(3).setCellValue(r.getPoints()); row.createCell(4).setCellValue(r.getGoodsName() null ? : r.getGoodsName()); row.createCell(5).setCellValue(new SimpleDateFormat(yyyy-MM-dd HH:mm).format(r.getCreateTime())); } // 直接写入响应流不落地文件 response.setContentType(application/vnd.ms-excel); response.setHeader(Content-Disposition, attachment; filenamepoint_report_ new SimpleDateFormat(yyyyMMdd).format(new Date()) .xls); workbook.write(response.getOutputStream()); }参数说明用HSSFWorkbook.xls格式而非XSSFWorkbook.xlsx因后者内存占用大而超市报表数据量通常1万行response.getOutputStream()直接输出省去服务器磁盘IO提升并发能力。4. 避坑指南部署、运行、调试中踩过的5个真实血泪坑4.1 现象Tomcat启动报错java.lang.ClassNotFoundException: com.mysql.jdbc.Driver原因项目用的是MySQL 5.x驱动com.mysql.jdbc.Driver但你装了MySQL 8.x默认驱动类名已改为com.mysql.cj.jdbc.Driver且URL需加?serverTimezoneUTC。解决① 替换WEB-INF/lib/mysql-connector-java-5.1.47.jar为8.x版本② 修改DBUtil.java中Class.forName(com.mysql.cj.jdbc.Driver)③ JDBC URL改为jdbc:mysql://localhost:3306/jf?serverTimezoneUTCuseSSLfalse。4.2 现象登录后跳转到404地址栏显示http://localhost:8080/jf/success.jsp原因项目打包为jf.war但Tomcat默认解压到webapps/jf/目录而JSP中跳转写的是绝对路径/success.jsp少了一级上下文路径。解决在所有RequestDispatcher跳转前加项目名如req.getRequestDispatcher(/jf/success.jsp).forward(req, resp)或统一用EL表达式${pageContext.request.contextPath}/success.jsp。4.3 现象积分兑换时提示“库存不足”但数据库里goods.stock明明是100原因goods表的stock字段为INT UNSIGNED而DAO中reduceStock()方法用ps.setInt(1, stock - 1)当stock0时0-1-1导致setInt(-1)抛异常但异常被吞掉返回false。解决在reduceStock()方法开头加校验if (currentStock 0) throw new SQLException(库存不足)或改用UPDATE goods SET stock stock - 1 WHERE id ? AND stock 0让SQL层保证原子性。4.4 现象导出Excel中文乱码显示“涓枃”原因response.setContentType(application/vnd.ms-excel)缺少字符集声明且POI写入时未指定编码。解决①response.setContentType(application/vnd.ms-excel;charsetUTF-8)② 在workbook.write(...)前加workbook.setSheetName(0, 积分流水, HSSFWorkbook.ENCODING_UTF_16)。4.5 现象同一张卡连续扫码两次积分加了两份但流水只有一条原因前端按钮未置灰用户快速连点两个请求几乎同时到达ServletMemberDAO.findByCardNo()查到同一会员各自执行updatePoints()造成ABA问题先读A再读A都按A更新。解决在updatePoints()的UPDATE SQL中加入版本号或时间戳校验如UPDATE member SET points ?, version version 1 WHERE id ? AND version ?或更简单——在Servlet中加synchronized(this)块仅限单机高并发需Redis分布式锁。5. 二次开发实战3个高频改造需求从改配置到加功能5.1 需求1把“满100元积1分”改成“满88元积1分”并支持不同会员等级不同倍率这是最常被问的问题。改动点极小只需修改DAO层一处// dao/MemberDAO.java 中 updatePoints() 方法内 // 原逻辑 // int addPoints (int) Math.floor(amount / 100); // 新逻辑先查会员等级再乘倍率 int basePoints (int) Math.floor(amount / 88); // 基础积分按88元计算 int level getCurrentLevel(memberId); // SELECT level FROM member WHERE id ? int multiple 1; if (level 2) multiple 12; // 白银1.2倍 → 存12避免浮点 else if (level 3) multiple 15; else if (level 4) multiple 20; int addPoints basePoints * multiple;验证方法用测试账号等级2消费176元应得floor(176/88)*12 2*12 24分消费87.9元应得0分。改完重新编译class无需重启Tomcat热替换生效。5.2 需求2增加“积分过期”功能过期积分自动归零每月1号凌晨执行项目没用Quartz但提供了util/SchedulerUtil.java作为定时任务入口。改造步骤在SchedulerUtil.java的start()方法中添加Timer timer new Timer(); timer.scheduleAtFixedRate(new TimerTask() { Override public void run() { Calendar cal Calendar.getInstance(); if (cal.get(Calendar.DAY_OF_MONTH) 1 cal.get(Calendar.HOUR_OF_DAY) 0) { expirePoints(); // 新增方法 } } }, 0, 24 * 60 * 60 * 1000); // 每24小时检查一次新增expirePoints()方法在MemberDAO中public void expirePoints() throws SQLException { // 过期规则距离现在365天前的IN类型流水且未被兑换goods_id IS NULL String sql UPDATE member m JOIN (SELECT member_id FROM point_record WHERE record_type IN AND create_time DATE_SUB(NOW(), INTERVAL 365 DAY) AND goods_id IS NULL GROUP BY member_id) r ON m.id r.member_id SET m.points GREATEST(m.points - COALESCE(r.expired_points, 0), 0); // 注实际需先统计每个会员过期积分再更新此处为简化示意 }注意生产环境建议用数据库事件MySQL Event Scheduler或独立调度服务避免Web容器线程阻塞。5.3 需求3对接微信扫码登录替换原用户名密码登录这是架构升级点。不推倒重来而是渐进式改造保留原登录入口LoginServlet不删新增WechatLoginServlet微信回调地址在weixin.jsp中放微信JS-SDK扫码按钮扫码后跳转WechatLoginServlet?codexxx获取OpenIDWechatLoginServlet用code向微信接口https://api.weixin.qq.com/sns/oauth2/access_token换取access_token和openid绑定逻辑查member表是否有该openid有则登录无则创建新会员卡号自动生成如WX202405200001并跳转绑定手机号页。// servlet/WechatLoginServlet.java 片段伪代码 String code request.getParameter(code); String tokenUrl https://api.weixin.qq.com/sns/oauth2/access_token? appid APPID secret SECRET code code grant_typeauthorization_code; // 调用微信接口获取JSON响应 JSONObject res JSON.parseObject(HttpUtil.get(tokenUrl)); String openid res.getString(openid); // 绑定或登录 Member member memberDAO.findByOpenid(openid); if (member null) { member new Member(); member.setOpenid(openid); member.setCardNo(WX new SimpleDateFormat(yyyyMMdd).format(new Date()) String.format(%04d, getNextSeq())); // 生成卡号 memberDAO.insert(member); } // 登录成功存session request.getSession().setAttribute(member, member); response.sendRedirect(request.getContextPath() /index.jsp);改造技巧所有新功能都通过新增Servlet实现不侵入原有逻辑微信相关密钥APPID/SECRET抽到config.properties文件避免硬编码。从那以后我每次接手Java Web老项目第一件事就是打开web.xml看Servlet映射第二件事是翻DBUtil.java确认驱动和URL第三件事是跑一遍jf.sql看表结构注释——这三步做完80%的“跑不起来”问题就定位了。希望帮到你。本文还有配套的精品资源点击获取