ARTICLE DETAIL

资讯详情

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

从JSP到积分流水:超市会员积分管理系统的实现与部署指南

从JSP到积分流水:超市会员积分管理系统的实现与部署指南 简介一份适合计算机专业学生毕业设计、期末大作业或实战练习的超市会员积分管理系统项目资料基于JSP与SSM框架开发覆盖会员信息管理、积分累计兑换、消费记录查询等核心业务。压缩包共987个文件约12.03MB含Java、JSP、CSS、JavaScript等前后端代码PNG、JPG、GIF等页面素材以及SQL脚本、XML与properties配置文件结构按模块划分清晰便于快速定位代码与配置目前已有41人学习下载项目经导师审定和严格调试源码可本地编译运行。除可运行源码外还提供论文、开发文档和数据文档分别阐述系统设计思路、数据库表结构、积分规则与测试过程能帮助学习者理解从需求分析到部署验证的完整链路。对需要完成课程设计和毕业答辩的学生来说这套资料具备较高的实用价值。1. 超市会员积分管理系统JSP 项目为什么至今仍是毕设主力“JSP 是过时技术”这句话在技术社区被喊了快十年但你去毕设选题库再搜一圈超市会员积分管理系统这类题目依然是常青树。这套系统听起来朴素内核却很典型会员开卡、消费收银、积分累计与兑换、商品与报表管理是一条完整的 JavaWeb 业务闭环正好长在课程设计和毕业设计的评分点上。技术栈也固定JSP Servlet MySQL跑在 Tomcat 上结构直白页面看得见、改得动答辩时从数据库讲到页面链路正好讲清楚。适合两类人手里已经有一个 jsp.zip 却不知道怎么在本地跑起来的人以及想参考一套现成的模块划分与代码组织自己动手搭毕设的人。2. 先拆业务再搭架子会员积分系统的模块边界与积分规则拿到任何一套管理系统第一件事不是看代码是先把业务边界画清楚。超市会员积分系统看着功能多核心只有四个动作办卡、消费、积攒积分、消耗积分。所有页面和表结构都围绕这四个动作转。2.1 核心流程先走一遍办卡、消费、兑换、对账办卡是起点。收银员录入姓名和手机号系统生成一张会员卡。卡号建议用时间戳加序号比如 202501101530001避免用自增主键暴露会员规模也方便收银员口述核对。办卡后这个会员就有了独立档案积分从 0 开始累计。消费是积分的主要来源。收银台录入商品、结算时输入会员卡号系统拿到实付金额按预设规则计算本次应得积分写一条积分流水同时更新会员表里的积分余额。这个过程涉及两个表的写入是最容易出数据问题的环节后面第四章会展开讲代码。兑换是积分的消耗出口。会员可以用积分抵扣现金、换赠品常见比例是 100 积分抵 1 元。兑换时要先校验积分是否够用扣减积分并写一条新的流水。对账则是每天或每周把积分流水汇总跟会员表里的余额比对数字对不上就说明中间有漏记或重复记账。这几个流程走完核心表就清楚了会员表存身份和积分余额积分流水表存每一笔变动订单表存消费记录兑换记录表存积分消耗去向。余额是结果流水是过程对账靠的是把过程和结果对齐。很多课设项目偷懒只做一张会员表积分字段直接加减短期看也能跑但答辩时一旦被问“这个月的积分为什么多了 3000 分”你拿不出任何凭证只能当场翻车。所以流水表是这个系统的底牌再麻烦也要建。2.2 角色与功能清单管理员和收银员各自的边界这套系统的用户角色通常分两档管理员和收银员。管理员管全局收银员管日常操作。角色权限一般不需要引入 Spring Security 这种重型框架在 Session 里存一个 role 字段页面渲染时判断一下必要时加一个 Filter 做登录拦截就够了。角色核心功能典型页面说明管理员商品管理、会员管理、积分规则配置、报表统计商品列表、会员列表、积分流水查询能看全量数据能手工调整积分收银员开卡、消费收银、积分查询与兑换收银台、开卡页、积分兑换页日常业务操作看不到成本与报表会员查询端个人信息展示、积分流水查询会员详情页一般只读不开放自助操作商品管理是容易被忽略的模块。积分兑换需要商品消费收银也需要商品所以商品表要同时服务两条业务线。给商品表加一个字段标记“是否允许积分兑换”比另外建一张兑换商品表更简单也更符合课设体量。报表统计在课设里不需要做得多复杂几张聚合页面就够把今日消费总额、今日发放积分、本月兑换积分几项列出来视觉效果和答辩素材都有了。功能边界画清楚后再去翻压缩包里的代码你就能快速定位每个类归属在哪条业务线上。如果解压后发现代码没有分层Servlet 里直接写 JDBC、JSP 里嵌 Java 代码也不用慌那是老式写法跑通之后想重构可以按第四章的分层思路慢慢拆。2.3 积分规则这么定后面才不吵架积分规则决定了两张核心表怎么设计。常见做法是消费 1 元积 1 分向下取整再乘以会员等级倍率。为什么要向下取整因为 0.99 元也按 1 分发放等于把抹零的成本转嫁给了超市不合适。会员等级按累计积分自动升级普通会员 1 倍积分银卡 1.2 倍金卡 1.5 倍等级越高买得越多积分滚得越快。这些倍率不要写死在代码里定义成一个常量类或者数据库配置表都行至少改起来不用翻遍全文。积分的有效期也要提前想好。很多真实超市是年度清分第二年会员积分作废。课设阶段不用强制做但在积分流水表里预留一个 expire_at 字段后面想加这个功能不用改表结构。兑换比例建议固定为 100 积分抵 1 元比例可配置意味着更多测试点答辩时反而说不清。这些规则定下来后积分流水表需要至少这几个字段变动积分正数增加、负数扣减、变动类型消费赠送、兑换扣减、手工调整、过期清分、关联订单号、发生时间、变动后余额。有了“变动后余额”对账时可以按时间顺序重放流水跟会员表当前余额对比差一分都能查出来。这算是我从对账查了一整晚换来的血泪经验先写在这里后面遇到问题你就知道它值多少钱。3. 本地跑通这套 JSP 系统的四件套JDK、IDEA、Tomcat、MySQL很多人的第一个坎不是业务是环境。这套 JSP 系统对环境的要求并不高但版本搭配不对会让它跑不起来而且报错信息往往不直说问题。按下面的组合配可以少走一大段弯路。3.1 版本怎么搭不翻车JDK 1.8 Tomcat 8.5 MySQL 5.7版本搭配是 JSP 项目的第一道玄学门槛。我一般推荐 JDK 1.8 Tomcat 8.5/9 MySQL 5.7这个组合兼容性最好。JDK 11 也能跑但部分老代码里调用了内省方法、JSTL 实现类在高版本 JDK 下会报模块访问错误课设阶段没必要给自己加戏。特别提醒JDK 1.8 不要配 Tomcat 10。Tomcat 10 把包名从 javax.servlet 换成了 jakarta.servlet而绝大多数 JSP 课设项目的源码里写的还是 javax.servlet直接用 Tomcat 10 启动会出现大量 ClassNotFoundException。如果压缩包里带了 README 或环境说明先按说明配没带说明直接按 JDK 8 Tomcat 8.5 来是安全牌。数据库用 MySQL 5.7 是因为对驱动类名最宽容。MySQL 8 的驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver老代码如果不改这行运行时才报错排查起来比写代码还费劲。这个坑到第五章单独说。如果你本机已经装了 MySQL 8不换版本也能跑改两行配置就行别急着卸载重装。IDEA 版本方面Ultimate 版自带 Tomcat 集成Community 版没有 Java Enterprise 插件但也可以新建普通 Java 项目后在框架支持里勾选 Web Application。如果你手上的压缩包是一个完整 Maven 工程IDEA 识别到 pom.xml 会直接按 Maven 项目打开不依赖这个插件。3.2 IDEA 里新建或导入 JSP 项目把 WAR 打包方式也说清楚拿到一个 .zip 压缩包第一步永远是解压。不要试图直接在压缩包里打开工程文件IDEA 对压缩包内的工程支持很差经常出现“项目结构识别不全、依赖全部飘红”的问题。解压后打开 IDEAFile - New - Project from Existing Sources选择解压出来的目录。如果目录里有 pom.xmlIDEA 会弹窗问你是不是导入 Maven 项目选 Import from external model - Maven等右下角依赖下载完成项目就进来了。如果你的需求是自己新建一个 JSP 项目练手路径是 File - New - Project - Java然后勾选 Web ApplicationIDEA 会生成 web 目录和 web.xml之后再手动添加 Tomcat 运行配置。手动配置的步骤是Run - Edit Configurations - 左上角加号 - Tomcat Server - Local在 Application server 里选中 Tomcat 安装目录然后切到 Deployment 选项卡把 Artifacts 里带 war exploded 后缀的那一项添加进去Application context 改成根路径 /。这样访问地址就是 http://localhost:8080不用带一长串项目名。WAR 打包也要区分两种场景。开发调试用 war exploded它把项目按目录结构直接输出改完 JSP 刷新浏览器就能看到效果最终交付或部署用 war 包IDEA 里执行 Build - Build Artifacts - 选择 war 包产物在 out 目录里。Maven 项目更简单执行 clean package 就会在 target 目录生成 .war 文件把这个文件丢到 Tomcat 的 webapps 下启动 Tomcat 它会自动解压部署。热词里有人搜“传统 jsp 项目打包 war”说的就是这个流程。3.3 建库建表与数据库连接初始化脚本和 JDBC 参数项目能编译不代表能跑数据库还得先建好。用客户端连上 MySQL执行下面的初始化脚本核心表一次建齐-- 初始化数据库使用 utf8mb4 避免中文乱码 CREATE DATABASE IF NOT EXISTS supermarket_db DEFAULT CHARACTER SET utf8mb4; USE supermarket_db; -- 会员表card_no 是卡号points 是当前可用积分total_points 是累计积分 CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT 会员卡号, name VARCHAR(50) NOT NULL COMMENT 会员姓名, phone VARCHAR(11) DEFAULT COMMENT 手机号, level TINYINT DEFAULT 1 COMMENT 会员等级 1普通 2银卡 3金卡, points INT DEFAULT 0 COMMENT 可用积分余额, total_points INT DEFAULT 0 COMMENT 历史累计积分, status TINYINT DEFAULT 1 COMMENT 状态 1正常 0挂失, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 开卡时间 ) ENGINEInnoDB COMMENT会员表; -- 商品表可用于收银结算也可用于积分兑换 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, product_no VARCHAR(20) NOT NULL UNIQUE COMMENT 商品编码, name VARCHAR(100) NOT NULL COMMENT 商品名称, price DECIMAL(10,2) NOT NULL COMMENT 销售价格, points_price INT DEFAULT 0 COMMENT 积分兑换价0表示不支持兑换, stock INT DEFAULT 0 COMMENT 库存, status TINYINT DEFAULT 1 COMMENT 状态 1上架 0下架 ) ENGINEInnoDB COMMENT商品表; -- 积分流水表每一笔积分变动都有记录这是对账的凭证 CREATE TABLE point_log ( id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL COMMENT 会员ID, order_no VARCHAR(30) DEFAULT COMMENT 关联订单号, change_points INT NOT NULL COMMENT 变动积分正数增加负数扣减, balance_after INT NOT NULL COMMENT 变动后积分余额, change_type VARCHAR(20) NOT NULL COMMENT 类型消费赠送/兑换扣减/手工调整/过期清分, remark VARCHAR(200) DEFAULT COMMENT 备注, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 发生时间, expiring_at DATETIME DEFAULT NULL COMMENT 积分过期时间按需预留 ) ENGINEInnoDB COMMENT积分流水表;这段 DDL 里有两个设计点值得说。member 表的 points 和 total_points 是两个字段前者是当前可用积分后者是历史累计积分用户升级看的是后者兑换扣减只影响前者两者不会互相污染。point_log 表里特意加了 balance_after 字段这给对账留了后路任何时刻都能按时间重放流水检查每一笔变动后余额是否连贯。数据库连接工具类建议写成独立类所有 DAO 都从它拿连接。核心就是三行参数// DBUtil.java —— JDBC 连接工具类 package com.shop.util; import java.sql.*; public class DBUtil { // 修改成你本机的 MySQL 账号密码 private static final String URL jdbc:mysql://localhost:3306/supermarket_db ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { // MySQL 5.7 用 com.mysql.jdbc.DriverMySQL 8 改成 com.mysql.cj.jdbc.Driver Class.forName(com.mysql.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL驱动加载失败检查jar包); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement st, ResultSet rs) { // 关闭顺序: 先 ResultSet, 再 Statement, 最后 Connection if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (st ! null) { try { st.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }URL 里的参数每个都有具体作用不要顺手删掉。useUnicodetrue 和 characterEncodingutf8 必须成对出现单独写一个不起作用useSSLfalse 是为了关掉 MySQL 8 的 SSL 警告serverTimezoneAsia/Shanghai 只在 MySQL 8 下必须要有5.7 可以不加。这些参数如果配错报错信息五花八门最常见的“The server time zone value”和“Communications link failure”都从这里来。4. 从下单到积分入账Servlet、Service、DAO 三层链路与核心代码环境跑通只是热身这套系统真正值钱的地方是积分入账的代码组织。如果压缩包里的代码是 GoF 风格的分层结构那自然是好如果是 Servlet 里直接写 JDBC 的扁平写法你可以按下面这个结构重构一边重构一边把积分业务的逻辑吃透。4.1 JSP 表单到 Servlet请求路由与参数处理一套完整请求的路径是浏览器提交表单 - JSP 发起请求 - Servlet 接收参数 - Service 处理业务 - DAO 读写数据库 - 结果返回页面。以开卡为例JSP 表单长这样%-- register.jsp —— 会员开卡页面 --% % page contentTypetext/html;charsetUTF-8 languagejava % form action${pageContext.request.contextPath}/member/register methodpost label会员卡号 input typetext namecardNo value${param.cardNo}/label label姓名 input typetext namename value${param.name}/label label手机号 input typetext namephone value${param.phone}/label button typesubmit开卡/button /form c:if test${not empty error} p stylecolor:red${error}/p /c:ifaction 里用${pageContext.request.contextPath}拼接项目根路径这是一个非常重要的习惯。项目部署后如果挂在某个子路径下表单直接写 /member/register 会因为根路径不同而 404拼上 contextPath 才能在各种部署方式下都跑对。对应的 Servlet 负责参数接收和跳转控制// MemberRegisterServlet.java —— 会员注册接口 WebServlet(/member/register) public class MemberRegisterServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 读取中文参数前必须设置编码否则姓名会乱码 request.setCharacterEncoding(UTF-8); String cardNo request.getParameter(cardNo); String name request.getParameter(name); String phone request.getParameter(phone); MemberService service new MemberService(); if (service.exists(cardNo)) { request.setAttribute(error, 该卡号已存在请更换); request.getRequestDispatcher(/register.jsp).forward(request, response); return; } service.register(cardNo, name, phone); // 写操作完成后重定向防止刷新页面时重复提交 response.sendRedirect(request.getContextPath() /member/list); } }注意这个 Servlet 里的跳转方式。查重失败时用 forward 转发到原页面把 error 属性带回表单显示注册成功时用 sendRedirect 重定向到列表页。两者的差别在刷新行为上forward 后按 F5 会重新提交表单可能重复开卡重定向后 URL 已经变成列表页F5 只是刷新列表这才是安全的做法。所有写操作完成后都应该用重定向收尾这条规则适用于本系统的所有新增、修改、兑换接口。4.2 积分计算与兑换扣减事务、幂等、防超兑积分入账是整个系统最核心的方法代码挑战在于要同时保证三件事同一笔订单不能重复积分、积分流水和余额必须同时更新、兑换时不能超扣。下面这个方法展示了标准写法// PointService.java —— 消费积分入账 public synchronized boolean addPoints(int memberId, BigDecimal amount, String orderNo) { Connection conn null; try { conn DBUtil.getConnection(); // 关闭自动提交保证下面两步要么都成功要么都回滚 conn.setAutoCommit(false); // 幂等检查该订单是否已经积过分 if (pointLogDao.findByOrderNo(conn, orderNo) ! null) { conn.rollback(); return false; } // 按消费金额向下取整1元1分 int points amount.intValue(); // 查会员等级按倍率放大积分银卡1.2倍金卡1.5倍 Member member memberDao.findByPk(conn, memberId); if (member.getLevel() 2) { points (int) Math.floor(points * 1.2); } else if (member.getLevel() 3) { points (int) Math.floor(points * 1.5); } // 写入积分流水 pointLogDao.insert(conn, memberId, orderNo, points, member.getPoints() points, 消费赠送); // 更新会员积分余额 memberDao.addPoints(conn, memberId, points); conn.commit(); return true; } catch (SQLException e) { // 执行出错必须回滚避免流水和余额不一致 try { if (conn ! null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { // 关闭连接释放数据库资源 DBUtil.close(conn, null, null); } }这个方法里有三个关键点值得展开说。事务是整个方法的骨架memberDao.addPoints 和 pointLogDao.insert 必须用同一个 Connection连接池里拿两次连接就是两个独立事务积分流水写成功了余额没更新这种事在真实项目里出过一次就得通宵对账。幂等检查用订单号作为唯一键即使前端重复点击、Servlet 被调用两次第二次也会因为订单号已存在而直接回滚。synchronized 在单机部署下能挡住并发线程同时给同一会员积分但如果你做集群部署就得靠数据库行锁或乐观锁课设阶段不涉及。积分兑换是另一个方向扣减积分时用条件更新实现防超兑-- 只有积分足够时才更新返回影响行数0表示积分不足 UPDATE member SET points points - #{usePoints} WHERE id #{memberId} AND points #{usePoints};用一条 UPDATE 把判断和扣减合并而不是先 SELECT 查余额再 UPDATE因为两条语句之间有间隙两个请求同时进来会双双通过检查导致积分扣成负数。条件更新返回的影响行数如果为 0说明余额不够兑换失败Service 层直接返回友好提示即可。4.3 会员列表与个人信息展示页面JSTL EL 渲染业务数据拿到后展示页面的写法同样有讲究。很多老项目喜欢在 JSP 里写% for (...) { %这种 Scriptlet代码能跑但页面全是 Java 片段改一个字段要折腾半天。主流做法是用 JSTL 标签库加 EL 表达式把 Java 代码从页面里清出去。%-- member_list.jsp —— 会员列表, 用 JSTL 渲染 --% % page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table border1 tr th卡号/thth姓名/thth手机号/thth等级/thth可用积分/th /tr c:forEach items${memberList} varm tr td${m.cardNo}/td td${m.name}/td td${m.phone}/td td c:choose c:when test${m.level 2}银卡/c:when c:when test${m.level 3}金卡/c:when c:otherwise普通/c:otherwise /c:choose /td td a href${pageContext.request.contextPath}/member/detail?memberId${m.id}${m.points}/a /td /tr /c:forEach /table渲染列表前Servlet 端只需要做两件事查询出集合放进 request然后转发到页面。// MemberListServlet.java —— 查出所有会员并转发到列表页 WebServlet(/member/list) public class MemberListServlet extends HttpServlet { protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding(UTF-8); request.setAttribute(memberList, new MemberService().listAll()); request.getRequestDispatcher(/member_list.jsp).forward(request, response); } }EL 表达式取值失败时有一个高频问题页面上输出一片空白但后台不报错。原因是 EL 默认取不到值就不输出你查 Servlet 里的 setAttribute 的 key 写的 memberList页面里写的 memberList漏了后缀或者大小写不一致就静默失败。排查方法是浏览器右键查看源代码看该位置是空白还是空标签再去比对 Servlet 里 setAttribute 的 key基本都能找到原因。个人信息展示页面也就是会员详情页按卡号查该会员的积分流水列表展示开卡时间、累计积分、最近几笔变动记录用的渲染方式跟列表页完全一致会一个就会另一个。5. 让 JSP 项目在本地跑顺5 个高频坑与排查思路就算整体环境正常这个项目也还有几个固定的坑等着你踩。下面每条都按现象、原因、解决三个步骤写清楚遇到时对照排查大部分问题十分钟内能定位。5.1 JSP 源码直接显示在浏览器里部署工件没生效现象浏览器打开地址后页面没有渲染成 HTML而是直接显示% ... %开头的 JSP 源码。原因IDEA 的 Tomcat 配置里没有把项目部署到 Tomcat。Tomcat 不会主动去编译 IDEA 项目目录里的 JSP只认自己 webapps 下的部署产物。运行时如果 Deployment 列表为空Tomcat 根本不知道要发布哪个 Artifact。解决进入 Run - Edit Configurations选中你的 Tomcat 配置切到 Deployment 选项卡点加号添加 Artifact选择带 war exploded 后缀的那一项Application context 改成 /。改完先执行 Build - Build Artifacts 重新构建再重启 Tomcat。顺便提醒一个细节如果有多个 Tomcat 配置每次换项目运行都要检查一下 Deployment 里挂的是不是当前这个模块很多 404 就是这么来的。5.2 Tomcat 启动闪退、端口占用和 404现象点启动后控制台日志一闪而过Tomcat 没能起来或者起来后访问页面报 404。原因端口被其他程序占用时Tomcat 会在启动中途抛 Address already in use 并退出。404 则可能是访问路径和实际部署路径不一致比如你访问 http://localhost:8080/ 但项目实际挂在 /supermarket_war_exploded 下。解决端口占用优先改端口打开 Tomcat 安装目录下的 conf/server.xml找到 Connector 节点把 port8080 改成 8081改完重启。想看清楚闪退原因在 IDEA 里用命令行方式启动到 Tomcat 的 bin 目录执行 catalina.sh runWindows 是 catalina.bat run日志会一帧不落地打到前台。访问路径问题就按上一条把 Deployment 的 Application context 改为根路径 /。还有一个容易忽略的点JSP 的动态请求要交给 Tomcatnginx 并不支持直接执行 JSP它只负责转发本地调试直接访问 Tomcat 端口就够了不需要提前套一层 nginx。5.3 中文乱码页面、Servlet、数据库三层一起查现象会员姓名在页面上是问号或者数据库里存的是一串乱码页面和数据库各错各的。原因中文乱码从来不是单一位置的问题。JSP 文件本身的编码、Servlet 读取请求参数的编码、数据库连接的编码三层只要有一层不是 UTF-8链路就会断。解决三层一起设。JSP 文件头写contentTypetext/html;charsetUTF-8同时确认 IDEA 右下角文件编码是 UTF-8Servlet 里在读取任何参数前先调request.setCharacterEncoding(UTF-8)JDBC URL 带上characterEncodingutf8建库语句用 DEFAULT CHARACTER SET utf8mb4。改完清缓存重启IDEA 有时候记住了旧的编译产物不重启就会继续用上次的结果。乱码问题经常被当成玄学其实三层编码一一对上就好。5.4 MySQL 8 驱动连接失败驱动类名和 URL 参数变了现象程序能编译但一执行数据库操作就报 ClassNotFoundException或者 Communications link failure后面跟着一串 server time zone 的报错。原因MySQL 8 开始驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver老类名虽然在 5.7 时代通用但在 8.x 下不一定被加载同时 MySQL 8 的 JDBC URL 必须显式指定 serverTimezone否则它不知道按哪个时区解析时间戳。解决确认你的 mysql-connector-java 版本。如果换成了 8.x 的 jar 包把 DBUtil 里的 Class.forName 改成com.mysql.cj.jdbc.DriverURL 里加serverTimezoneAsia/Shanghai和useSSLfalse。5.7 环境则继续用老类名不要画蛇添足。这个坑最容易让人怀疑人生因为日志里的堆栈指向的是底层 socket 连接看起来像网络问题其实只是驱动参数不匹配。5.5 积分被重复计算不重定向、不加事务的代价现象一笔订单提交成功后刷新页面或者重新点击下单按钮积分翻了一倍。原因两个操作叠加导致。Servlet doPost 里用了 forward 转发到成功页刷新页面时浏览器重新提交了表单同时积分入账的方法没有做幂等检查同一订单号可以反复入账。两条原因命中任何一条积分都会重复。解决前端操作上写请求的 Servlet 处理完一律 sendRedirect把浏览器地址变成了列表页刷新就不会再次提交表单后端逻辑上点积分入账方法里先按订单号查流水表存在就直接返回 false这相当于在数据层加了一道保险。前端防误触、后端防重复两道叠加才算堵住。事务是另一重保障入账方法里流水和余额更新必须放在同一个事务里任何一个失败都回滚否则你遇到的将不是积分翻倍而是流水跟余额对不上这种更头疼的问题。6. 再多走一步积分效期、统一登录和对账验证跑通只算及格在答辩前还能把它推进几步这几步恰恰是评审最愿意听的内容。6.1 给积分加上一年有效期积分流水表已经预留了 expiring_at 字段现在把它用起来。消费赠送的积分写入时设置过期时间为一年后再用 MySQL 事件调度器每月执行一次清分任务把过期积分从余额里扣掉同时补一条“过期清分”的流水记录账。事件调度器方案在单机课设里够用了不需要引 Quartz。-- 每月1日凌晨2点执行过期积分清分任务 CREATE EVENT IF NOT EXISTS clear_expired_points ON SCHEDULE EVERY 1 MONTH STARTS 2025-01-01 02:00:00 DO UPDATE member m LEFT JOIN ( SELECT member_id, SUM(change_points) AS expiring_points FROM point_log WHERE change_type 消费赠送 AND expiring_at NOW() GROUP BY member_id ) tmp ON tmp.member_id m.id SET m.points m.points - IFNULL(tmp.expiring_points, 0);这条 SQL 对账的关键是清分只影响余额不删流水任何时候都能解释积分去向。6.2 用 Filter 做统一登录校验项目成型后页面越来越多不可能在每个 JSP 里重复写判断 Session 的代码。一个登录拦截 Filter 解决// LoginFilter.java —— 未登录用户统一跳转到登录页 WebFilter(/*) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 登录接口和静态资源放行其余请求必须登录 if (uri.endsWith(/login) || uri.endsWith(.js) || uri.endsWith(.css) || uri.endsWith(login.jsp)) { chain.doFilter(req, resp); } else { if (request.getSession().getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); } else { chain.doFilter(req, resp); } } } }放了这一段代码后未登录请求直接访问 /member/list 也会被踢回登录页权限问题一次性收口。6.3 消费退货的对账验证改造做完用最简单的方式验证数据正确性开一张卡做一笔消费 100 元的订单查积分流水应增加 100 分会员表的 points 同时变成 100再做一笔退货 50 元的操作流水里出现一条 -50 的冲正记录会员表的 points 变成 50。把流水表按时间求和得到 50跟余额比对一致这套数据模型才是闭环。我自己的教训是越早把流水表设计进去答辩前越从容——积分账对不上的时候流水表就是你最后的后悔药。希望帮到你。本文还有配套的精品资源点击获取
返回列表