简介:这是一套基于Javaweb的超市会员管理系统毕业设计项目,面向计算机相关专业正在做毕设的学生,以及需要项目实战练习的Java学习者,也可作为课程设计或期末大作业使用。系统采用JSP、Servlet、JDBC配合MySQL数据库,开发环境为JDK、Eclipse和Tomcat,功能涵盖会员管理、超市运营等核心模块,界面简洁、操作便捷。压缩包共742个文件、约15.72MB,以jpg/png图片、java源码、js脚本、css样式、html页面和sql数据库脚本为主,辅以xml配置与项目说明,目录清晰,方便导入开发工具直接运行。资源经严格调试确保可运行,附带数据库脚本、软件工具与项目使用说明,帮助快速搭建环境并理解完整实现逻辑。目前已有668人学习下载,适合需要完整毕业设计参考方案或实战练习的读者。
1. 这套Javaweb超市会员管理系统:毕设能跑、课设能交、练习能上手
做毕设最怕的不是题目难,而是拿到一套号称能运行的源码,导入Eclipse后满屏红叉,Tomcat一启动就报ClassNotFound。这套基于Javaweb的超市会员管理系统,是我近期拆过的最省心的一套JSP+Servlet+JDBC老牌技术栈项目。后台没有花哨的Spring全家桶,就是最经典的JSP页面、Servlet控制层、JDBC操作MySQL,界面用Bootstrap框架搭的,功能覆盖会员管理、商品管理、积分兑换这些超市核心业务场景。对计算机专业正在做毕业设计的学生来说,它可以直接当毕设交;对想练Javaweb基本功的初学者来说,它比SSM项目更好读懂请求流转过程。项目自带数据库脚本和工具包,配置好环境就能跑。
2. 系统功能拆解与技术选型:为什么JSP+Servlet这套老技术还能打
2.1 功能模块全览:会员、商品、积分、消费记录
这套系统解压后,先从web.xml入手看了一遍映射关系,能清楚看到它把超市会员业务拆成了几个核心模块。
| 模块 | 核心功能 | 对应Servlet/Dao |
|---|---|---|
| 会员管理 | 会员注册、信息查询、余额充值 | MemberServlet、MemberDao |
| 商品管理 | 商品录入、上下架、库存修改 | GoodsServlet、GoodsDao |
| 消费结算 | 会员购物结账、积分累计 | CheckoutServlet |
| 积分管理 | 积分累计、积分兑换商品/优惠 | ScoreServlet |
| 系统管理 | 管理员登录、密码修改 | AdminServlet |
这个功能划分很典型:每个业务域对应一个Servlet接收请求,再转给DAO层操作数据库。JSP只负责展示数据,没有把业务逻辑写在页面里——这是这套源码比很多培训机构的“纯JSP项目”强的地方。会员管理是核心链路,从会员注册到消费积分再到积分兑换,闭环完整,答辩时讲解器来非常顺手。
2.2 为什么选Servlet+JDBC而不是MyBatis
现在Javaweb毕设很多用Spring Boot+MyBatis,但这套系统坚持用Servlet+JDBC,反而成了优势。至少有三点值得说:
第一,包结构一目了然。controller、dao、entity、util四个包把职责分清楚,Entity对应数据库表字段,DAO层写JDBC代码,Servlet里做请求参数封装和转发跳转。这比Spring Boot里动不动就十几层目录结构好理解得多。
第二,能清楚看到一次HTTP请求从JSP表单到Servlet再到数据库的完整路径。比如会员注册功能,JSP里form表单提交到MemberServlet的doPost方法,Servlet里调用MemberDao的addMember方法,DAO里用PreparedStatement执行SQL。这是最原始的Javaweb请求流程,面试笔试常考,答辩时老师问“数据是怎么存进去的”,直接对着代码讲就行。
第三,对做课设的学生来说,JDBC手动管理连接能让你真正理解数据库连接池存在的意义。项目里用的是DriverManager获取连接,性能上不如连接池,但对于几百条测试数据量的课程设计完全够用,而且不需要额外配置c3p0或Druid依赖,环境复杂度更低。
2.3 项目目录结构与配置文件说明
拿到项目后,理解目录结构是第一步。
SupermarketMember/ ├── src/ │ ├── com/supermarket/entity/ # 实体类:Member.java、Goods.java等 │ ├── com/supermarket/dao/ # 数据访问层:JDBC操作 │ ├── com/supermarket/servlet/ # 控制层:接收请求、跳转页面 │ ├── com/supermarket/util/ # 工具类:DBUtil.java、MD5Util.java │ └── db.properties # 数据库连接配置 ├── WebContent/ │ ├── css/ # Bootstrap、自定义样式 │ ├── js/ # jQuery、jQuery UI │ ├── images/ # 图片资源 │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # JSTL等依赖jar包 │ └── *.jsp # 页面文件 └── sql/ └── supermarket.sql # 数据库初始化脚本重点关注db.properties,这是数据库连接的关键配置。常见做法是把数据库驱动类、连接URL、用户名、密码都放在这个文件里,DBUtil读取Properties文件来创建连接。我一般建议学生改完这个文件后先单独写个测试类跑一下连接,不要一上来就启动Tomcat。
3. 数据库设计与初始化:从SQL脚本到关系梳理
3.1 核心数据表设计
这套系统的数据库脚本里包含了六张核心表,表间关系紧密,覆盖了超市会员业务的主体流程。第一次打开sql目录下的supermarket.sql,别急着执行,先看表结构。
-- 管理员表 CREATE TABLE admin ( admin_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员ID', username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录账号', password VARCHAR(64) NOT NULL COMMENT '密码(MD5加密)' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 会员表 CREATE TABLE member ( member_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '会员ID', card_no VARCHAR(20) NOT NULL UNIQUE COMMENT '会员卡号', name VARCHAR(50) NOT NULL COMMENT '姓名', phone VARCHAR(11) COMMENT '手机号', balance DECIMAL(10,2) DEFAULT 0.00 COMMENT '账户余额', score INT DEFAULT 0 COMMENT '积分', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表 CREATE TABLE goods ( goods_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID', goods_name VARCHAR(100) NOT NULL COMMENT '商品名称', price DECIMAL(10,2) NOT NULL COMMENT '售价', stock INT DEFAULT 0 COMMENT '库存', status TINYINT DEFAULT 1 COMMENT '1上架 0下架' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这个建表语句有几个细节值得注意:一是字符集用了utf8mb4而不是utf8,避免了生僻字和特殊符号存不进去的隐患;二是余额字段用DECIMAL(10,2)而不是FLOAT,金额计算不会出现精度误差——这一点在答辩时如果被问到“为什么不用double”,能答上来会很加分;三是会员卡号设置了UNIQUE约束,防止重复办卡。
3.2 表关系与业务流转
会员表与积分明细表、消费记录表是多对一关系。消费记录表是关键表,它关联会员和商品,结构大致如下:
CREATE TABLE consume_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, member_id INT NOT NULL COMMENT '所属会员', goods_id INT NOT NULL COMMENT '购买商品', amount DECIMAL(10,2) NOT NULL COMMENT '消费金额', score_earned INT DEFAULT 0 COMMENT '获得积分', consume_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_record_member FOREIGN KEY (member_id) REFERENCES member(member_id), CONSTRAINT fk_record_goods FOREIGN KEY (goods_id) REFERENCES goods(goods_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;理解了表关系,看代码就有方向了。会员消费时做的事情是:插入一条消费记录、更新会员表里的余额和积分、扣减商品库存。这三步操作在DAO层里分别对应三个方法,设计中比较理想的做法是把它们放到一个事务里,执行过程中任何一步失败都要回滚。这套源码里的事务处理分两种写法:CheckoutServlet里的操作在Service层包裹了事务控制,而部分简单的更新操作是单条SQL执行——复现时建议重点看消费结算这条链路的事务处理。
3.3 初始化数据库的两种方式
执行SQL脚本有两种方式,我用Navicat和命令行都跑通过。
方式一,用Navicat或其他可视化工具导入:
打开Navicat,创建数据库supermarket,字符集选utf8mb4,然后右键选择“运行SQL文件”,选中项目附带的supermarket.sql。执行完毕后在表列表里能看到全部六张表以及预置的测试数据。
方式二:命令方式执行:
mysql -uroot -p123456 < sql/supermarket.sql参数说明:-u后面跟MySQL用户名,-p后面跟密码(注意-p和密码之间没有空格)。执行后可以用SHOW TABLES;确认一下是否建表成功。
3.4 数据库连接配置与JDBC工具类
数据库建好后,修改src目录下的db.properties:
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456我强烈建议你把serverTimezone=Asia/Shanghai加上去,MySQL 8.x版本如果不指定时区,连接时会直接报The server time zone value '�й���ʱ��' is unrecognized,这个报错我见过太多学生踩过。另外useSSL=false也建议保留,本地开发环境不需要SSL加密,省去证书相关的麻烦。
DBUtil工具类里的代码是老套路,加载驱动、读取Properties、创建连接:
public class DBUtil { private static String driver; private static String url; private static String username; private static String password; // 静态代码块只执行一次:加载配置 static { try { Properties props = new Properties(); InputStream in = DBUtil.class.getClassLoader().getResourceAsStream("db.properties"); props.load(in); driver = props.getProperty("jdbc.driver"); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName(driver); } catch (Exception e) { e.printStackTrace(); } } // 获取连接 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这段代码的逻辑:静态代码块在类加载时执行,把db.properties里的四项配置读进静态变量,然后Class.forName注册驱动。后续DAO层直接调用DBUtil.getConnection()就能拿到连接。
4. 项目导入与运行避坑:Eclipse+Tomcat环境下的常见问题排查
4.1 导入Eclipse的具体步骤
这套项目在Eclipse中导入时,按标准动态Web项目的方式处理。新手最容易在这一步卡住。
File -> Import -> General -> Existing Projects into Workspace,然后Select root directory选中项目根目录(就是包含pom.xml或.project文件的层级),Finish完成导入。
如果导入后没有显示成Dynamic Web Project,右键项目 -> Properties -> Project Facets,勾选Dynamic Web Module 3.0和Java 1.8,点击Apply。这一步很关键,很多同学导入后看到的是普通Java项目,JSP文件没有语法高亮,就是Project Facets没配置对。我一般会顺手检查一下Java Compiler的版本是否与JDK匹配,如果不匹配会直接导致编译失败。
4.2 Tomcat版本选择与启动
这套系统是Servlet 3.0规范,Tomcat 7及以上都能跑,但我推荐用Tomcat 8.5。Tomcat 9和10对老项目的兼容性有时会出问题,尤其是javax.servlet包名在Tomcat 10里换成了jakarta.servlet——如果项目里import的是javax.servlet,Tomcat 10直接编译报错。这是血泪经验。
# 检查Tomcat是否正常启动 sudo sh startup.sh # 或者Windows下双击startup.bat # 启动后访问 http://localhost:8080/supermarket/login.jsp如果端口被占用,在Tomcat安装目录的conf/server.xml里修改端口:
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />把8080改成8088或其他空闲端口,改完重启Tomcat再访问http://localhost:8088/supermarket/login.jsp。
4.3 三个高频报错与解决方案
现象一:启动Tomcat时报ClassNotFoundException: com.mysql.jdbc.Driver。
原因:MySQL驱动jar包没有放到WEB-INF/lib目录下,或者放到了但没被编译。IDE里虽然能看到jar包,但运行时找不到是部署配置问题。
解决:确认WEB-INF/lib目录里有mysql-connector-java的jar包,然后在Eclipse里右键项目 -> Properties -> Deployment Assembly,点击Add -> Java Build Path Entries,把MySQL驱动jar包加进去,保存后重新发布项目。不同IDE的Project Structure里也需要确认Artifacts中是否包含了这个jar包。
现象二:页面中文乱码。
原因:jsp页面编码、MySQL连接参数、表字符集三层中至少有一层没统一。
解决:三步走。第一步把db.properties的jdbc.url加上characterEncoding=utf8;第二步确认建表时用了utf8mb4字符集;第三步在JSP页面顶部加上:
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>如果还乱码,检查一下HTML的<meta charset="UTF-8">标签。这四层统一后,乱码问题基本绝迹。
现象三:修改了Java代码但运行时页面没变化。
原因:Tomcat没有热部署,或者IDE里没勾选自动编译。
解决:Eclipse里双击Servers视图中的Tomcat实例,在Server Options里勾选Serve modules without publishing和Automatically reload when resources change。改代码后手动Ctrl+S保存,再刷新浏览器。要注意的是,修改web.xml或lib目录下的jar包时还是要重启Tomcat,JSP和Java类的热部署范围是有限制的。
4.4 MySQL 8.x兼容性排查
很多学生机器上装的是MySQL 8.x,而老项目用的驱动可能是5.x版本。常见做法是换用与MySQL 8.x匹配的驱动:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>同时确认db.properties里的driver类名是com.mysql.cj.jdbc.Driver,不要漏掉中间的cj。MySQL 8下还要注意时区参数,这个前面提过了。另外MySQL 8默认的认证插件是caching_sha2_password,老版本驱动可能不兼容,如果连接时报Auth plugin错误,需要给MySQL用户改回兼容的加密规则:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '123456';这是一条非常实用的命令,写完这条再重启Tomcat,连接就回来了。
5. 核心业务代码走读:会员注册、消费结算的实现链路
5.1 会员注册:从JSP表单到DAO入库
会员注册是整个系统最基础也最完整的链路。JSP页面接收用户输入,表单提交到MemberServlet:
@WebServlet("/member/register") public class MemberRegisterServlet extends HttpServlet { private MemberDao memberDao = new MemberDao(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 设置编码,防止中文乱码 request.setCharacterEncoding("UTF-8"); // 2. 接收表单参数 String name = request.getParameter("name"); String phone = request.getParameter("phone"); String cardNo = request.getParameter("cardNo"); // 3. 封装实体对象 Member member = new Member(); member.setName(name); member.setPhone(phone); member.setCardNo(cardNo); member.setBalance(0.0); member.setScore(0); // 4. 调用DAO层入库 boolean success = memberDao.addMember(member); if (success) { response.sendRedirect(request.getContextPath() + "/member/list"); } else { request.setAttribute("errorMsg", "注册失败,请检查信息"); request.getRequestDispatcher("/member/register.jsp").forward(request, response); } } }这段代码的要点:第一步一定要先设置request编码,否则中文姓名和手机号在参数获取阶段就乱码了;第三步手动封装实体对象,这是Servlet时代最标准的写法,后续框架里的自动参数绑定就是替代了这个枯燥的过程;第四步根据DAO层返回值决定跳转还是转发——重定向是重新发起一次请求,能避免表单重复提交,转发则保留request域里的数据用来显示错误信息。这个差异经常被问,建议记住。
MemberDao里的addMember方法,用PreparedStatement执行插入:
public boolean addMember(Member member) { String sql = "INSERT INTO member(card_no, name, phone, balance, score) VALUES(?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, member.getCardNo()); ps.setString(2, member.getName()); ps.setString(3, member.getPhone()); ps.setDouble(4, member.getBalance()); ps.setInt(5, member.getScore()); return ps.executeUpdate() > 0; } catch (SQLException e) { e.printStackTrace(); return false; } }这里用了try-with-resources语法,连接和PreparedStatement在try结束后自动关闭,不用手动写finally关了,是JDK 7引入的写法。PreparedStatement有个天然优势——预编译防SQL注入,参数用?占位,调用setString时驱动会处理特殊字符,所以不要用字符串拼接SQL,这是老项目里最常见的坑。
5.2 消费结算:事务与积分计算
消费结算是整套系统里逻辑最复杂的模块。会员购买商品时,涉及到三张表同时更新:
public boolean checkout(int memberId, int goodsId, int quantity) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 1. 查询商品信息(单价、库存) Goods goods = goodsDao.getGoodsById(conn, goodsId); double totalAmount = goods.getPrice() * quantity; // 2. 更新商品库存 int stock = goods.getStock() - quantity; goodsDao.updateStock(conn, goodsId, stock); // 3. 更新会员余额 memberDao.updateBalance(conn, memberId, totalAmount); // 4. 插入消费记录,累计积分 int earnedScore = (int)(totalAmount / 10); consumeDao.insertRecord(conn, memberId, goodsId, totalAmount, earnedScore); memberDao.updateScore(conn, memberId, earnedScore); conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { e.printStackTrace(); try { conn.rollback(); // 一旦出错,全部回滚 } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { try { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } catch (SQLException e) { e.printStackTrace(); } } }事务的核心在于setAutoCommit(false)之后,所有SQL都驻留在同一个事务中,只有调用了commit才真正落盘。如果在第3步更新余额失败,第2步已经改掉了库存,这时候必须rollback把库存改回来,否则就出现“库存减了但钱没扣”的数据不一致。这是数据库事务ACID里一致性最典型的体现,答辩时讲这个例子非常加分。
积分计算规则是每消费10元累计1分,(int)(totalAmount / 10)这个写法对金额做了向下取整——消费99元得9分。这个规则在业务层写死了,如果题目要求改成“消费1元累计1分”,只需修改这一行的除数。
5.3 分页查询与模糊搜索
会员列表页用了jQuery UI和Bootstrap框架做的分页组件,后端DAO层是标准的LIMIT分页查询:
public List<Member> findMembers(String keyword, int pageNum, int pageSize) { List<Member> members = new ArrayList<>(); StringBuilder sql = new StringBuilder("SELECT * FROM member WHERE 1=1"); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.isEmpty()) { sql.append(" AND (name LIKE ? OR phone LIKE ? OR card_no LIKE ?)"); String pattern = "%" + keyword + "%"; params.add(pattern); params.add(pattern); params.add(pattern); } sql.append(" LIMIT ?, ?"); params.add((pageNum - 1) * pageSize); params.add(pageSize); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } ResultSet rs = ps.executeQuery(); while (rs.next()) { Member m = new Member(); m.setMemberId(rs.getInt("member_id")); m.setName(rs.getString("name")); m.setPhone(rs.getString("phone")); m.setCardNo(rs.getString("card_no")); m.setBalance(rs.getDouble("balance")); m.setScore(rs.getInt("score")); members.add(m); } } catch (SQLException e) { e.printStackTrace(); } return members; }这里用了StringBuilder拼接SQL,两边加%实现模糊匹配。用WHERE 1=1构造动态条件,是为了后续不管有没有keyword都能直接拼接。LIMIT后面的(pageNum-1)*pageSize是分页的核心计算,第一页从第0条开始取,第二页从第10条开始取(假设pageSize=10)。这个写法要理解,答辩时老师经常会问“分页为什么是从0开始而不是从1”。
6. Tomcat部署上线技巧:把毕设从本地搬到服务器
6.1 导出WAR包手动部署
本地跑通只是第一步,答辩演示时的稳妥做法是直接在Tomcat的webapps目录下运行。在Eclipse里右键项目 -> Export -> WAR file,导出一个supermarket.war包,放到Tomcat的webapps目录下。启动Tomcat后它会自动解压部署,访问路径就是http://你的IP:8080/supermarket/。
6.2 生产环境下的关键调优
本地开发环境追求能跑就行,但部署到答辩演示的服务器或虚拟机时,有些细节值得调整,也很容易在答辩时被问到。
连接池替换:在本机测几十条数据时,每次手动建连接完全可行,但一旦数据量上来或者并发访问量变大,频繁创建连接的开销不可忽视。常见做法是把源码里的DriverManager连接方式换成DBCP或Druid连接池。用Druid时,只需要在DBUtil的静态代码块里初始化一个DruidDataSource,后续getConnection时从池里拿:
private static DruidDataSource dataSource = new DruidDataSource(); static { try { dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUrl("jdbc:mysql://localhost:3306/supermarket?..."); dataSource.setUsername("root"); dataSource.setPassword("123456"); dataSource.setInitialSize(5); dataSource.setMaxActive(20); } catch (Exception e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }Druid连接池的核心参数:initialSize是启动时创建的物理连接数,maxActive是最大连接数,超过这个数的新请求就要排队等待连接归还。讲课设项目时,把DriverManager替换成连接池原理说清楚,这段属于加分项。
JVM内存参数:如果Tomcat在答辩演示的过程中页面上有大表格或图片可能导致卡顿,可以在catalina.sh里调整JVM启动参数:
JAVA_OPTS="-Xms512m -Xmx1024m -XX:MaxPermSize=256m"这个参数的意义:JVM初始分配512MB堆内存,最大允许1024MB。堆内存过小时频繁触发Full GC会导致页面卡顿,设大一些能缓解大数据量加载时的压力。
MySQL乱码一劳永逸方案:修改MySQL配置文件my.ini(Windows)或my.cnf(Linux)底部的[mysqld]段落,加入:
[mysqld] character-set-server=utf8mb4 collation-server=utf8mb4_unicode_ci改完后重启MySQL服务,新创建的数据库默认就是utf8mb4,不需要每条建表语句都手动指定字符集。这个统一配置比在JDBC连接上加characterEncoding参数更彻底,因为后者只对当前连接生效,前者对所有连接生效。
6.3 答辩演示前的验证清单
最后分享一个我每次上线前都会跑一遍的检查流程。先是登录管理员账号,试着新增一个会员,再去消费结算,最后看记录;把窗口关掉重开,确认数据还在——验证持久化没问题。改掉db.properties里的数据库密码,重启项目,确认正常用新密码连接——这一步是为了防止答辩时换了机器漏改了配置。再确认一下WEB-INF/lib目录下的mysql驱动版本与服务器MySQL版本匹配,MySQL 8就配8.0.x的驱动,别混用。这套流程走下来大概也就十分钟,但能避开九成以上的现场翻车。从那以后,我每次帮学生部署毕设项目,都强制自己先跑一遍这套验证清单再收工。希望这些细节能帮到你,少走点我当年走过的弯路。
本文还有配套的精品资源,点击获取