简介:这是一套基于Java Web的超市管理系统完整项目资料,面向计算机相关专业的在校学生、课程设计或毕业设计需求者,以及希望以真实项目练手的小白与进阶学习者。项目已通过导师指导与答辩评审,获得95分成绩,代码经过测试可正常运行,适合直接用于课设、毕设、作业或项目初期立项演示。压缩包共125个文件,约3.05MB,以30个Java源文件、20个JSP页面、23个JavaScript脚本、10个XML配置及若干CSS、图片资源为主,另含war包与说明文档,覆盖后端逻辑、前端交互与数据库配置等模块。目前已有40人学习关注。读者可获得一套结构清晰、可直接部署运行的超市管理系统源码,并借助详细文档理解各模块职责与实现思路,在已有基础上修改扩展功能,快速完成课程设计或毕业设计任务。
1. 从一份 95 分课设包说起:Java web 超市管理系统到底交付了什么
如果你正在搜「web课程设」或者「数据库课程设计mysql」,大概率是手里压着一个必须交差的项目,时间不多,又不想随便糊弄。这份基于 Java web 的超市管理系统,就是那种典型的、能直接跑起来、能对着文档改、能拿去答辩的完整课设包。它不是那种只有几个空壳类的半成品,而是把超市日常经营里最核心的几条业务线——商品管理、库存进出、订单结算、供应商维护——都串起来了,后端用 Servlet 做控制层,Service 层封装业务逻辑,DAO 层对接数据库,前端配了样式和日期控件,整体是一个能演示、能讲清楚数据流向的闭环。
它适合谁?计算机相关专业的在校生,尤其是数据库课程设计、Java web 课程设计、软件工程课程设计这几类场景,拿它当底稿最省事。你不需要从零搭环境,也不用纠结表结构怎么设计才合理,因为包里已经有一套跑通的方案。对熟手来说,它的价值在于分层清晰、命名规范,改起来不费劲,想加个会员积分或者促销模块,顺着现有结构往下写就行。下面我按「先看懂结构、再动手跑通、最后避开坑」的顺序,把这份资源拆开讲。
2. 分层结构与数据库设计:为什么这套 Servlet + Service + DAO 值得照着改
2.1 三层架构的职责边界与选型理由
这套系统没有用 Spring 全家桶,而是老老实实手写 Servlet、Service、DAO 三层。很多人一看没有框架就觉得「土」,但对于课程设计来说,这恰恰是优点:每一层的职责肉眼可见,答辩时老师问「请求怎么走到数据库的」,你能指着代码一行行讲清楚。Servlet 负责接收 HTTP 请求、做参数校验、控制跳转;Service 层放业务规则,比如「库存不足时不允许下单」「删除供应商前先检查有没有关联商品」;DAO 层只做 SQL 执行和结果集映射,不掺业务判断。
这种分法的好处是改需求时定位快。比如要加一个「商品打折」功能,你只需要在 Service 层加折扣计算逻辑,DAO 层加一个更新价格的 SQL,Servlet 层加一个接收折扣参数的入口,三层各改各的,互不污染。常见做法是每一层都先定义接口再写实现,包里能看到ProviderService接口和ProviderServiceImpl实现类成对出现,这就是典型的面向接口编程,方便后面替换实现或者做单元测试。
数据库这边,超市管理系统的表不会太多,但关系要理清楚。核心表一般包括商品表、分类表、供应商表、库存记录表、订单表和订单明细表。商品表通过分类 ID 关联分类表,通过供应商 ID 关联供应商表;订单表通过订单明细表关联商品表,形成多对多。这里有个设计细节值得注意:库存不要只存在商品表的一个字段里,最好单独建库存流水表,每次入库出库都记一条,这样对账和追溯都方便,也是数据库课设里容易拿分的点。
2.2 建库建表与连接配置的落地步骤
先把数据库建起来。下面这段 SQL 覆盖了最核心的几张表,字段类型和约束都按 MySQL 的常见写法给,你可以直接拿去执行,也可以按自己学校的命名规范微调。
-- 创建数据库,字符集用 utf8mb4 避免中文乱码 CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; -- 供应商表 CREATE TABLE provider ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '供应商名称', contact VARCHAR(50) COMMENT '联系人', phone VARCHAR(20) COMMENT '联系电话', address VARCHAR(200) COMMENT '地址' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名称' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品表,关联分类和供应商 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '商品名称', category_id INT COMMENT '分类ID', provider_id INT COMMENT '供应商ID', price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '售价', stock INT NOT NULL DEFAULT 0 COMMENT '当前库存', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES category(id), FOREIGN KEY (provider_id) REFERENCES provider(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT '订单编号', total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, price DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES orders(id), FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;执行完建表语句后,往分类表和供应商表里插几条测试数据,不然商品表的外键没法填。接着处理 Java 侧的数据库连接。包里通常会把连接参数放在一个 properties 文件或者一个工具类里,我一般习惯用db.properties加一个DBUtil工具类,这样换数据库或者改密码不用翻遍代码。
# db.properties jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码// DBUtil.java 关键片段 public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); Class.forName(props.getProperty("jdbc.driver")); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); } catch (Exception e) { throw new RuntimeException("数据库配置加载失败", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }这里有几个参数要盯住。serverTimezone=Asia/Shanghai不加的话,MySQL 8 以上版本经常报时区错误,这是血泪经验。characterEncoding=utf8配合建库时的utf8mb4,能挡住大部分中文乱码。驱动类名从 MySQL 5 的com.mysql.jdbc.Driver换成了com.mysql.cj.jdbc.Driver,用错版本会直接抛ClassNotFoundException。连接池这块,课设里用不用都行,如果老师要求体现「mysql的数据库连接池」这个知识点,可以引入 Druid 或者 HikariCP,把DriverManager换成DataSource.getConnection(),其余 DAO 代码基本不用动。
2.3 DAO 层增删改查的写法与参数绑定
DAO 层是数据库课设的评分重灾区,写得好不好一眼就能看出来。下面以商品查询和新增为例,展示一个规范的 DAO 写法,重点是PreparedStatement的使用和资源关闭。
// ProductDao.java 片段 public class ProductDao { // 根据分类查询商品,演示参数绑定 public List<Product> findByCategory(int categoryId) { List<Product> list = new ArrayList<>(); String sql = "SELECT id, name, price, stock FROM product WHERE category_id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, categoryId); // 参数从 1 开始计数 try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); list.add(p); } } } catch (SQLException e) { e.printStackTrace(); } return list; } // 新增商品,返回自增主键 public int insert(Product p) { String sql = "INSERT INTO product(name, category_id, provider_id, price, stock) VALUES(?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setString(1, p.getName()); ps.setInt(2, p.getCategoryId()); ps.setInt(3, p.getProviderId()); ps.setBigDecimal(4, p.getPrice()); ps.setInt(5, p.getStock()); int rows = ps.executeUpdate(); if (rows > 0) { try (ResultSet keys = ps.getGeneratedKeys()) { if (keys.next()) { return keys.getInt(1); } } } } catch (SQLException e) { e.printStackTrace(); } return -1; } }逻辑说明:try-with-resources保证 Connection、PreparedStatement、ResultSet 自动关闭,避免连接泄漏,这是课设里最容易被扣分的地方。参数绑定用setInt、setString、setBigDecimal,不要用字符串拼接,否则既有 SQL 注入风险,又会在遇到单引号时翻车。Statement.RETURN_GENERATED_KEYS用来拿自增主键,下单时先插订单主表拿到订单 ID,再插订单明细,这个顺序不能反。金额字段用BigDecimal而不是double,避免浮点精度问题,这也是数据库课设里体现基本功的细节。
3. 从部署到跑通:Servlet 映射、页面跳转与订单结算链路
3.1 项目导入与 Tomcat 部署的关键配置
拿到 zip 之后,先解压看目录结构。典型的 Eclipse 动态 Web 项目会有src、WebContent(或webapp)、build几个目录,WebContent下面有WEB-INF、css、js、img等。包里出现的style.css、public.css、datepicker.css、WdatePicker.css说明前端用了统一的样式表和日期选择控件,img.gif是图标资源。这些静态资源要放在能被 Tomcat 直接访问的路径下,一般就是WebContent根目录或者其子目录。
导入步骤我一般这么走:Eclipse 里File -> Import -> Existing Projects into Workspace,选中解压后的项目根目录。如果项目没有.project文件,就新建一个 Dynamic Web Project,把src和WebContent的内容拷进去。接着配 Tomcat:Window -> Preferences -> Server -> Runtime Environments添加本地 Tomcat 目录,版本建议 8.5 或 9.0,太新的 Tomcat 10 把javax.servlet换成了jakarta.servlet,老代码会大面积报错,这是新手最容易踩的坑。
部署时右键项目Run As -> Run on Server,如果控制台报ClassNotFoundException: com.mysql.cj.jdbc.Driver,说明 MySQL 驱动 jar 没放进WEB-INF/lib。驱动版本要和 MySQL 服务器版本匹配,MySQL 8 用mysql-connector-java-8.x.jar,MySQL 5 用5.1.x。放好之后重启 Tomcat,访问http://localhost:8080/项目名/看首页能不能出来。
3.2 Servlet 映射与请求流转的调试方法
Servlet 的映射有两种写法:web.xml配置和注解@WebServlet。这份包里两种都可能出现,如果改了 Servlet 类名或者 URL 却访问不到,先确认映射有没有同步改。下面是一个商品列表 Servlet 的典型写法。
@WebServlet("/product/list") public class ProductListServlet extends HttpServlet { private ProductService productService = new ProductServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 接收分类参数,没传就查全部 String categoryId = req.getParameter("categoryId"); List<Product> products; if (categoryId != null && !categoryId.isEmpty()) { products = productService.findByCategory(Integer.parseInt(categoryId)); } else { products = productService.findAll(); } req.setAttribute("products", products); req.getRequestDispatcher("/product_list.jsp").forward(req, resp); } }逻辑说明:@WebServlet("/product/list")把 URL 绑定到这个类,浏览器访问/product/list就会进doGet。req.getParameter拿查询参数,这里做了空判断,避免Integer.parseInt(null)抛异常。查到的数据用setAttribute放进请求域,再forward到 JSP 渲染。调试时如果页面空白,先看 Tomcat 控制台有没有异常堆栈;如果报 404,检查 URL 拼写和项目上下文路径;如果报 500,多半是 Service 或 DAO 里空指针或者 SQL 语法错误。
订单结算这条链路最能体现系统完整性。用户在前端选商品、填数量,提交到OrderServlet,Servlet 先校验库存,再调用 Service 层开启事务:插入订单主表、插入订单明细、扣减商品库存,三步要么全成功要么全回滚。事务控制放在 Service 层,用Connection.setAutoCommit(false)手动提交,DAO 层共用同一个 Connection。这里有个常见翻车点:如果每个 DAO 方法各自从连接池拿连接,事务就失效了,必须把 Connection 作为参数传下去,或者用 ThreadLocal 绑定当前线程的连接。
3.3 前端样式与日期控件的接入细节
前端部分虽然不复杂,但样式错乱会直接影响答辩观感。style.css和public.css一般是全局样式,datepicker.css和WdatePicker.css是日期选择控件 My97DatePicker 的配套样式。接入时注意三点:CSS 文件的引入路径要用项目上下文路径,比如<link rel="stylesheet" href="${pageContext.request.contextPath}/css/style.css">,直接写相对路径在 forward 之后容易 404;日期控件的 js 要在页面底部引入,且WdatePicker()的调用要绑在输入框的onfocus上;img.gif这类图片资源如果路径写死成绝对路径,换机器部署就会裂图,统一用${pageContext.request.contextPath}开头最稳。
4. 避坑与排查:课设跑不起来时先查这五处
4.1 中文乱码从请求到响应逐层排查
现象:页面输入中文商品名,存进数据库变成问号,或者查询出来显示乱码。原因通常有三层:数据库字符集不是 utf8mb4、JDBC URL 没带 characterEncoding、Servlet 响应没设 ContentType。解决顺序是先从数据库查起,SHOW VARIABLES LIKE 'character%'确认character_set_server和character_set_database都是 utf8mb4;再检查db.properties里的 URL 是否带useUnicode=true&characterEncoding=utf8;最后在 Servlet 里resp.setContentType("text/html;charset=utf-8"),并且req.setCharacterEncoding("utf-8")要放在读取任何参数之前。三层都对齐,乱码基本绝迹。
4.2 外键约束导致删除失败
现象:删除供应商或者分类时,控制台报Cannot delete or update a parent row: a foreign key constraint fails。原因是商品表里还有记录引用了这个供应商或分类。解决方式有两种:一是业务层先查关联商品数量,有引用就不让删,给用户提示;二是建表时把外键加上ON DELETE SET NULL或ON DELETE CASCADE,但级联删除风险大,课设里更推荐第一种,顺便还能体现业务逻辑的完整性。我一般会在 Service 层写一个canDeleteProvider(int id)方法,返回布尔值,Servlet 根据结果决定跳转提示页还是执行删除。
4.3 Tomcat 端口占用与上下文路径冲突
现象:启动 Tomcat 报Address already in use: JVM_Bind,或者访问项目 404。原因是 8080 端口被别的程序占了,或者项目上下文路径和访问 URL 对不上。解决:改 Tomcat 的conf/server.xml里 Connector 的 port,换成 8081 或 9090;上下文路径在 Eclipse 的Properties -> Web Project Settings里看,默认是项目名,访问时要带上,比如项目名是supermarket,URL 就是http://localhost:8080/supermarket/。如果改了项目名没改上下文,就会一直 404。
4.4 日期格式转换异常
现象:提交订单时填了日期,后台报java.text.ParseException或者IllegalArgumentException。原因是前端传的是字符串,后端用SimpleDateFormat解析时格式没对上,比如前端给的是2024-01-01,后端按yyyy/MM/dd解析。解决:统一前后端日期格式,前端日期控件设置dateFmt:'yyyy-MM-dd',后端SimpleDateFormat("yyyy-MM-dd"),并且解析时用 try-catch 包住,格式不对就返回友好提示,不要让异常直接抛到页面。
4.5 连接未关闭导致 Tomcat 启动越来越慢
现象:反复调试几次后,Tomcat 启动变慢,甚至报Too many connections。原因是 DAO 里 Connection 没关,连接池被耗尽。解决:所有数据库操作必须用 try-with-resources,或者 finally 里显式 close。检查时可以在 MySQL 里执行SHOW PROCESSLIST,看有没有大量 Sleep 状态的连接堆积。如果用了连接池,配置里加上maxActive、maxIdle、validationQuery,让空闲连接能被回收和检测。
5. 二次开发与答辩加分:把课设改成能讲出彩的版本
跑通只是及格线,想拿高分得在现有骨架上加东西。我一般会从两个方向下手:一是补一个「库存预警」功能,商品表加一个min_stock字段,每次出库后检查当前库存是否低于阈值,低于就在首页弹提示,这个功能涉及 SQL 条件查询、Service 判断、前端展示,一条链路全走通,答辩时能讲三分钟。二是把订单查询做成带分页和条件筛选的,用LIMIT加COUNT实现,前端加日期范围和订单号模糊查询,这能体现你对数据库分页和动态 SQL 的掌握。
验证功能是否真的生效,别只看页面能点。我的习惯是每加一个功能,先在数据库里手动造几条边界数据:库存为 0 的商品、没有关联商品的供应商、金额为 0 的订单,然后走一遍流程看系统怎么反应。边界不崩,功能才算稳。答辩前把web.xml、db.properties、建表 SQL 和核心 Servlet 的代码打印出来,老师问哪块翻哪块,比现场翻 IDE 从容得多。
从那以后我每次拿到课设包,都强制先跑一遍建表 SQL、再确认驱动版本、最后走一遍下单链路,三步过了才动手改代码。希望帮到你。
本文还有配套的精品资源,点击获取