Java+MySQL超市管理系统:Servlet/JSP/JDBC分层实现
2026/9/15 6:03:48 网站建设 项目流程

简介:这是一个基于Java与MySQL的Web超市信息管理系统项目源码包,主要面向高校计算机专业学生,尤其适合毕业设计、课程设计、期末大作业等实战场景。项目经导师指导并通过高分评审,代码完整、配置齐全,代码注释与层次划分清晰,基础薄弱的学习者也能按文档完成本地部署,快速理解JavaWeb项目的典型开发流程。压缩包共64个文件,包含28个Java源文件、21个JSP页面、10个XML配置文件,另有数据库脚本、项目说明文档、界面预览图与Maven工程信息等,整体约593KB,目录结构简洁标准,便于按功能模块定位代码。资源中的SQL脚本可一键导入MySQL,配置文件与说明文档能快速构建运行环境;文档梳理了系统功能与运行注意事项,可支撑二次开发和答辩讲解。目前已有83人学习下载,对需要完整可运行课设源码并想弄清实现细节的同学,是很实用的参考资料。

1. 为什么这个超市管理系统适合做Java期末大作业

期末前两周收到一份zip,里面就三样东西:pom.xml、sms.sql、src目录,外加一个README。时间紧任务重,最怕的不是代码写不出来,而是技术选型飘。有的同学临时换成Spring Boot加Vue,演示很好看,可答辩老师一句“MyBatis的Mapper代理是如何生成SqlSession的”就把路堵死了。这套基于Java和MySQL的Web超市信息管理系统走的是Servlet加JSP加JDBC的老路线,链路短、每一段都看得见摸得着:HTTP请求进Servlet,Servlet调Service,Service调DAO,DAO执行SQL,MySQL返回结果。业务上它把商品、分类、订单、库存串成一条完整回路,既能演示一个超市的进销存过程,也能支撑“库存为什么不会变负数”“订单金额为什么要冗余”这类偏进阶级的追问。适合正在做毕业设计、课程设计、期末大作业的计算机相关专业学生,也适合想快速补齐Java Web底子的练习者。

2. MySQL数据库脚本:sms.sql中的表关系与初始化数据

先剖数据,因为Java代码写得再规范,表结构设计不合理,后面所有查询都会被迫绕路。

2.1 五张表如何覆盖超市进销存

sms.sql的核心由五张表组成:user、category、product、orders、order_item。为什么订单要拆成主表和明细两张?因为一次结账会包含多个商品,主表orders记录“这一单多少钱、谁收银、什么时间”,明细表order_item逐行记录“卖了哪个商品、单价多少、数量多少”。这样设计之后,统计某段时间各商品的销量,直接拿order_item按product_id聚合,跟orders表的状态变更互不干扰。

表名关键字段在业务流程里的作用
userid, username, password, role登录与权限,区分管理员和收银员
categoryid, name, description商品分类,下拉框数据源
productid, category_id, name, price, stock, status商品主数据,库存字段直接放这
ordersid, order_no, user_id, total_amount, status, create_time每次结账生成一条汇总
order_itemid, order_id, product_id, quantity, unit_price订单明细,冗余下单时的快照

user表的role建议直接存“ADMIN”或“CASHIER”字符串,不要用0和1,不然答辩时还得解释映射关系。product里的stock字段就是库存,我见过不少项目把库存单独建一张表,对课设来说没必要,stock放在product上,一次UPDATE就能完成扣减和判断。category_id在product表里只做一个逻辑外键,Java层校验分类是否存在,而不是用数据库的FOREIGN KEY去强约束,这样既能保持数据关系清晰,又不会在删除分类时被外键锁死。

2.2 建表细节:字符集、引擎和索引的三个坑

这是第一次开拆项目时最容易忽略的地方。CREATE DATABASE要用utf8mb4而不是utf8,utf8在MySQL里是utf8mb3,只有3字节,商品名里出现生僻字或emoji就会落库失败。引擎统一InnoDB,因为订单创建和库存扣减要开事务,MyISAM既没有行锁也不支持事务回滚。

建product表的DDL通常长这样:

CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID', category_id INT NOT NULL COMMENT '分类ID,逻辑外键', name VARCHAR(128) NOT NULL COMMENT '商品名称', price DECIMAL(10,2) NOT NULL COMMENT '售价', stock INT NOT NULL DEFAULT 0 COMMENT '库存', status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', KEY idx_category_id (category_id), KEY idx_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

字段类型就四种:INT、VARCHAR、DECIMAL、TINYINT。price用DECIMAL(10,2)而不是FLOAT,否则0.1加0.2会出现精度脏数据。索引方面,category_id加普通索引支撑分类筛选,name加索引给模糊查询加速。写SQL时要记住LIKE '可乐%'这种前缀匹配能走索引,LIKE '%可乐%'不会走索引,数据量小感觉不到,把测试数据撑到几万行后,商品列表页的响应差距一下就出来了,这也是答辩时值得主动抛出来的点。

2.3 初始化账号与验证导入结果

sms.sql末尾一般会带几组INSERT:一个admin管理员账号、几组商品分类、二十来条商品记录、一条演示用订单。密码字段放的是MD5摘要,不要存明文。这里有个细节,如果SQL里默认密码是123456,导入后要记得在用户管理页面改掉,防止演示时被老师抓到弱口令话题。

导入报错时先看两个地方:一是SQL文件编码是不是UTF-8,Windows下用记事本另存为很容易留成GBK;二是MySQL的sql_mode,如果开着ONLY_FULL_GROUP_BY,GROUP BY查询会把不是全字段的查询直接拒绝。导入完成后先用SQL验证行数:

SELECT COUNT(*) AS user_cnt FROM user; SELECT COUNT(*) AS product_cnt FROM product; SELECT COUNT(*) AS order_cnt FROM orders;

三个数字都对得上再启动Java项目,能省掉后面一多半排查时间。

3. Java Web分层落地:pom.xml、JDBC连接与DAO实现

数据层立住之后,Java端的结构决定后面每次改需求需要动几个类。zip里的pom.xml说明项目用Maven管理,而不是手动拷贝jar包的旧路子。

3.1 Maven依赖与provided作用域对比

依赖只保留四类:Servlet API、JSP API、MySQL驱动、Druid连接池。其中前两个scope要写成provided,表示编译时需要、但运行时由Tomcat提供,打完的war包不会重复塞进去。Druid是国内Java Web项目里用得很多的连接池之一,自带监控页,答辩时打开/druid/index.html能看到SQL执行统计,比口头说“我用了连接池”具体得多。

<dependencies> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet.jsp</groupId> <artifactId>javax.servlet.jsp-api</artifactId> <version>2.3.1</version> <scope>provided</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.49</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.23</version> </dependency> </dependencies>

数据库驱动版本和MySQL实例版本要对应。MySQL 5.7用com.mysql.jdbc.Driver,MySQL 8.x必须换成com.mysql.cj.jdbc.Driver,还要在URL里加serverTimezone=Asia/Shanghai,否则连接时直接报时区异常。页面连不上数据库,优先查这一条。

3.2 db.properties与Druid数据源初始化

数据库连接参数不该写死在Java类里,常见做法是放到src/main/resources/db.properties:

driverClassName=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false username=root password=123456 initialSize=2 maxActive=10

再写一个DbUtils工具类,加载属性并初始化连接池:

public class DbUtils { private static DruidDataSource dataSource; static { try { Properties props = new Properties(); props.load(DbUtils.class.getClassLoader() .getResourceAsStream("db.properties")); dataSource = (DruidDataSource) DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }

初始化放在静态块里,类第一次被引用时执行一次,不会每个请求都重开连接。initialSize表示启动时预建的物理连接数,maxActive是最大活跃连接数,课设场景2和10完全够用。createDataSource接收的Properties键名就是driverClassName、url、username、password,写错任何一个都会在getConnection时才抛异常,所以排查顺序放在Tomcat日志之后、业务代码之前的第二顺位。

3.3 DAO层:PreparedStatement与ResultSet映射

DAO层把一行行ResultSet变成Java对象。商品表的按ID查询是最常用的方法,代码风格基本反映了整个项目的数据访问水平:

public Product findById(int id) { String sql = "SELECT id, category_id, name, price, stock, status " + "FROM product WHERE id = ?"; try (Connection conn = DbUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setCategoryId(rs.getInt("category_id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); p.setStatus(rs.getByte("status")); return p; } } } catch (SQLException e) { e.printStackTrace(); } return null; }

两点要注意。第一,PreparedStatement用?占位再setInt绑定,不用字符串拼SQL,这是防SQL注入的第一道门。第二,try-with-resources保证Connection、PreparedStatement、ResultSet三个资源在方法结束后自动关闭,比手动finally少一个漏关的风险点。

项目里Servlet、Service、DAO的职责分配可以归纳成下表:

典型类职责
Web层LoginServlet, ProductServlet接收请求参数,分发到JSP
业务层OrderService开事务、校验库存、调用多个DAO
数据层ProductDao, OrderDao, UserDao只负责SQL执行与结果集映射

分层不是规定动作,直接让Servlet里写JDBC也能跑,但那样一来,下单要扣库存时就必须在Servlet里拼两个SQL,老师追问“事务边界在哪”就答不上来。有Service层挡在中间,事务和回滚逻辑才有地方放。

4. 页面交互与业务闭环:登录会话、商品CRUD与订单库存联动

分层代码要跑起来,靠三条主线:登录、商品管理、下单。每一条线都对应一个Java Web关键点。

4.1 登录过滤器与Session会话控制

登录成功后把User对象放进Session,但每个页面都判断Session里当前用户是否为空太啰嗦,应该用一个Filter统一拦:

@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; HttpSession session = request.getSession(false); String uri = request.getRequestURI(); if (uri.endsWith("/login") || uri.endsWith("login.jsp") || uri.endsWith(".css") || uri.endsWith(".js")) { chain.doFilter(req, resp); return; } if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(req, resp); } }

注意白名单的设计:登录接口、登录页、静态资源直接放行,其余请求一律检查Session。这里有个细节,getSession(false)只在已有会话时返回对象,没有就返回null,避免给匿名访问者白白创建会话。Filter拦截的是路径,放行条件必须覆盖项目实际资源后缀,否则CSS加载不出来页面布局全乱。

4.2 商品分页与模糊查询的参数计算

商品列表不能一次把几千行全塞到页面上,JSP项目分页靠LIMIT子句实现:

SELECT id, category_id, name, price, stock, status FROM product ORDER BY id LIMIT ?, ?;

第一个?是偏移量,第二个?是每页条数。当前第currentPage页、每页pageSize条时,偏移量计算方式为:

int offset = (currentPage - 1) * pageSize;

比如第2页每页10条,offset就是10,即从第11条开始取10条。模糊搜索和分页组合的SQL是:

SELECT * FROM product WHERE name LIKE CONCAT('%', ?, '%') ORDER BY id LIMIT ?, ?;

用CONCAT('%', ?, '%')而不是Java端先拼好再传,这样查询条件里如果出现%或_这类通配符,MySQL会按字面量处理,不会破坏查询边界。分页参数和计算结果可以在答辩时用表格直接展示:

参数含义示例值
currentPage当前页码2
pageSize每页条数10
offsetLIMIT偏移量,公式为(currentPage-1)*pageSize10
totalCountCOUNT(*)查询结果25
totalPages总数除以每页条数后向上取整3

分页要写两个方法,一个返回List,一个返回COUNT计数,因为MySQL不会在同一个结果集里既返回数据又返回总数。

4.3 订单提交:库存扣减与事务回滚

下单的操作本质是:在同一个数据库连接里先减库存、再写订单明细、最后更新订单主表,任何一步失败都整体回滚。

public boolean createOrder(Connection conn, OrderVO vo) throws SQLException { conn.setAutoCommit(false); try { for (OrderItem item : vo.getItems()) { int rows = productDao.decreaseStock(conn, item.getProductId(), item.getQuantity()); if (rows == 0) { conn.rollback(); return false; } orderItemDao.insert(conn, item); } orderDao.insert(conn, vo.getOrder()); conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }

decreaseStock方法对应的SQL是关键:

UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?;

受影响行数为0说明当前库存小于扣减数量,直接回滚。这种写法没有单独SELECT库存再判断,用一行带条件的UPDATE完成“先查后写”,是防库存超卖的常用手段。注意事务必须全程在同一个Connection上完成,如果decreaseStock和insertOrder各拿一个连接,即便开了事务也没有原子性。这一块是整个项目最值得现场演示的部分,因为肉眼能在商品列表看到库存数字变化。

5. 部署到Tomcat的完整排错路径与答辩演示清单

临近验收,换一台机器部署环境就变了,最容易踩的坑集中在两处:SQL没有导对库,以及驱动版本与数据库不匹配。

5.1 导入SQL与启动环境验证

先在MySQL里建库,再导入sms.sql。直接从命令行执行mysql -u root -p < sms.sql会把表建到默认库,如果默认库名和项目里db.properties的url对不上,后续怎么调都连不上。

CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARSET utf8mb4; USE supermarket; SOURCE /path/to/sms.sql;

导入完成后依次执行三个COUNT查询,行数正常再启动Tomcat:

SELECT COUNT(*) FROM user; SELECT COUNT(*) FROM product; SELECT COUNT(*) FROM orders;

5.2 常见启动报错对照

排查顺序固定为:先看Tomcat日志,再核对数据库连接参数,不要一开始就怀疑业务代码。

报错特征常见原因处理方式
ClassNotFoundException: com.mysql.jdbc.Drivermysql-connector没打进war检查pom.xml依赖后重新打包
Access denied for user密码或主机不匹配在MySQL里确认root账号的host和密码
Table doesn't exist库选错或SQL没导入核对USE语句与建库语句
Communications link failure端口被占用或地址写错telnet localhost 3306验证连通性

5.3 演示节奏与一个现场验证技巧

演示顺序建议固定成一条有头有尾的业务链路:admin登录 → 新增分类 → 新增商品 → 商品列表翻页 → 收银台下单 → 回到商品列表确认库存减少 → 到订单管理查询订单。其中下单前后用SQL验证一次库存与订单明细的联动:

SELECT p.id, p.name, p.stock, (SELECT SUM(oi.quantity) FROM order_item oi WHERE oi.product_id = p.id) AS sold FROM product p WHERE p.id = 10;

执行前先记录stock值,下单后再执行一次,stock减少的数量等于sold新增的累加量,这个数字对比就是库存联动最好的证据。

如果老师追问权限控制,可以在LoginFilter里加一个角色判断分支,拦截CASHIER访问用户管理接口,这比重新开一套权限框架快得多。答辩时把这层逻辑顺着说完,比如“把白名单改成一个角色到路径的映射表就能扩展成多角色系统”,这个问题就落到了可实现的细节上。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询