news 2026/10/5 9:36:40

JavaWeb超市订单管理系统:从数据库设计到事务处理的课程设计全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JavaWeb超市订单管理系统:从数据库设计到事务处理的课程设计全解析

简介:一套 JavaWeb 超市订单管理系统课程设计源码与数据库资源包,面向准备提交课程设计、急需运行演示或学习 JavaWeb 分层开发的在校学生。系统围绕超市订单与商品管理等核心业务,采用 JSP + Servlet + JavaBean 架构,代码注释清晰,逻辑完整,可直接在 IDE 中运行并配合 SQL 脚本初始化数据库。资源包共 162 个文件,包含 34 个 Java 类、21 个 JSP 页面、23 个 JavaScript 文件,以及 1 份 SQL 数据库脚本,另配备 CSS 样式表、XML 配置与若干前端图片,整体仅 374KB,体积轻量但目录结构规整,便于快速定位关键模块。已有 310 人浏览学习,项目经多次课程设计实践验证,具备较高的完整性,基本无需修改即可演示,适合作为高分课程设计模板进行二次开发或拓展,也可用于熟悉订单管理系统的常规功能与交互流程。

1. javaweb超市订单管理系统:一个高分课程设计背后的完整链路

每年课程设计入库,javaweb超市订单管理系统都是被翻牌次数最多的题目之一。原因不复杂:它有典型的进销存业务逻辑——管理员登录、供应商维护、商品上架、订单创建与扣库存,技术栈刚好落在Servlet、JSP、JDBC、MySQL这些课堂重点上,既不会简单到没含金量,也不会复杂到一学期做不完。同一份题目交上去,分数可以差出二十分,差距往往不在代码行数,而在订单有没有事务保护、登录拦截有没有做、数据库表设计有没有冗余。

这篇笔记要解决的就一件事:拿到一份超市订单管理系统源码后,怎么把它跑通、看懂、讲清楚,并且照着公司里的分层习惯把代码改到能拿95分以上。我按表结构设计、环境搭建、核心代码、踩坑记录、答辩加分这个顺序写,还没选定题目的读者,看完这篇再决定要不要走这个方向也来得及。

2. 数据库建模先行:超市订单系统的表结构与建表SQL

2.1 功能模块怎么拆:从登录到订单的完整链路

先别急着看代码,把需求落成数据模型再动手,后面基本不返工。超市订单管理系统最常见的需求拆法是四个模块加一块:用户模块负责管理员登录和密码修改,简单版就一张用户表,登录页一个Servlet,Session里放一个User对象;商品模块负责商品的增删改查、名称模糊查询和上下架状态;供应商模块负责供应商的信息维护,和商品形成一对多关系;订单模块负责选商品、填数量、提交订单、生成订单主表和明细、同时扣减库存。

这四个模块里,订单模块是整个系统最考验数据库功底的部分,也是评分老师最常追问的地方。订单包含多个商品,天然是一对多结构,要把订单拆成主表和明细表两张才能完整表达。用户下单产生订单,订单包含多条订单明细,每条明细指向一个商品,商品归属一个供应商——实体关系一句话讲完,外键只加在订单明细和商品这一层,不往下继续级联,否则删数据会引发一串连锁反应。

2.2 六张核心表的建表SQL与字段设计理由

直接给建表语句。这里有个实际操作建议:把整个建库建表过程录屏,答辩时放给老师看,直接证明这份设计是你亲手做过而不是从别人zip里解压出来的。

-- 建库,字符集统一用 utf8mb4,MySQL 5.7 以上都支持 CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; -- 1. 用户表 CREATE TABLE `user` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT 'MD5加密后的密码', `real_name` VARCHAR(50) DEFAULT '' COMMENT '真实姓名', `role` VARCHAR(20) DEFAULT 'admin' COMMENT '角色,预留字段', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

CREATE_USER这块的逻辑说明:username加UNIQUE约束,防止登录名重复;password字段长度给到100而不是50,因为MD5摘要固定32位,但以后如果要换BCrypt算法,32位就不够放了,长度提前留好。create_time用DEFAULT CURRENT_TIMESTAMP,插入数据时Java代码里就不用手动new Date(),少写一行是一行。

-- 2. 供应商表 CREATE TABLE `supplier` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '供应商名称', `contact` VARCHAR(50) DEFAULT '' COMMENT '联系人', `phone` VARCHAR(20) DEFAULT '' COMMENT '联系电话', `address` VARCHAR(200) DEFAULT '' COMMENT '地址', `remark` VARCHAR(500) DEFAULT '' COMMENT '备注' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='供应商表'; -- 3. 商品表 CREATE TABLE `product` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL COMMENT '商品名称', `price` DECIMAL(10,2) NOT NULL COMMENT '销售单价', `stock` INT DEFAULT 0 COMMENT '库存数量', `unit` VARCHAR(20) DEFAULT '件' COMMENT '计量单位', `supplier_id` INT COMMENT '供应商ID', `status` TINYINT DEFAULT 1 COMMENT '1上架 2下架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_product_supplier FOREIGN KEY (supplier_id) REFERENCES supplier(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

商品表的price字段是重点。这里用DECIMAL(10,2)而不是float或double,是因为浮点数在计算机里是近似存储,0.1加0.2会得到0.30000000000000004这种结果,订单金额对账时差一分钱老师都会盯上。DECIMAL(10,2)表示最多8位整数加2位小数,超市商品单价到几百元完全够用。stock字段用INT不加UNSIGNED,是为了配合下单扣库存时的stock >= ?条件判断,库存不足时让数据库直接拒绝更新,比Java代码里先查再判断可靠。

-- 4. 订单主表 CREATE TABLE `orders` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_no` VARCHAR(40) NOT NULL UNIQUE COMMENT '业务订单号', `user_id` INT NOT NULL COMMENT '下单用户', `total_amount` DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '订单总金额', `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0新建 1已支付 2已取消', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES `user`(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; -- 5. 订单明细表 CREATE TABLE `order_item` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `order_id` INT NOT NULL COMMENT '归属订单', `product_id` INT NOT NULL COMMENT '下单商品', `quantity` INT NOT NULL COMMENT '购买数量', `price` DECIMAL(10,2) NOT NULL COMMENT '成交单价,区别于商品现价', CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id), CONSTRAINT fk_item_product FOREIGN KEY (product_id) REFERENCES product(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'; -- 6. 操作日志表(加分项,时间紧可以不加) CREATE TABLE `operation_log` ( `id` INT PRIMARY KEY AUTO_INCREMENT, `user_id` INT NOT NULL, `action` VARCHAR(500) NOT NULL COMMENT '操作内容描述', `ip` VARCHAR(50) DEFAULT '' COMMENT '客户端IP', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';

订单主表和明细表的设计有两个容易理解错的地方。第一,order_no单独用一个字段存业务单号,不用自增id当单号,因为自增id会暴露当天订单量,而且跨表查询时主键和外键都用id,业务单号只面向用户展示;order_no的生成规则后面第4章会给出代码。第二,order_item里的price字段存的是下单那一刻的成交单价,不是商品表的当前售价,这样商品日后改价,历史订单的金额依然算得清。

2.3 初始化数据与两个建表习惯

系统跑起来之前,至少要插入一个管理员账号、两条供应商数据、四五个商品,不然登录进去页面是空的,演示效果很差。初始化数据直接写在SQL文件里。

USE supermarket; -- 管理员账号,密码统一 123456,用MD5摘要存储 INSERT INTO `user` (`username`, `password`, `real_name`, `role`) VALUES ('admin', MD5('123456'), '系统管理员', 'admin'); -- 供应商样例数据 INSERT INTO `supplier` (`name`, `contact`, `phone`, `address`) VALUES ('华东食品供货商', '李经理', '13900001234', '城北工业园3号'), ('城北农产品合作社', '王姐', '13800004321', '城北农贸市场A区'); -- 商品样例数据,价格注意保持DECIMAL精度 INSERT INTO `product` (`name`, `price`, `stock`, `unit`, `supplier_id`) VALUES ('农夫山泉550ml', 2.00, 200, '瓶', 1), ('康师傅红烧牛肉面', 4.50, 150, '桶', 2), ('伊利纯牛奶250ml', 3.50, 300, '盒', 1), ('海天酱油500ml', 8.80, 80, '瓶', 2);

密码这里用MD5('123456')直接在SQL里完成摘要,登录时Java端也对输入做一次MD5再比对,数据库里永远不存明文密码。两个建表习惯值得沿用:一是每张表都有独立的id主键,其它表引用时只带id不带名称,改供应商名字时商品表不用动;二是表和字段都写了COMMENT,答辩时鼠标移到字段上就能讲出设计意图,不用临时翻代码。

3. 从源码到跑通:环境搭配与IDEA运行javaweb项目配置

3.1 环境版本怎么搭配最省事

一份课程设计源码拿到手,第一件事不是连数据库,而是确认Java和服务器版本。老源码用JDK 1.8写的情况最多,匹配Tomcat 8.5或9.0,MySQL 5.7和8.0都能跑。如果手头只有JDK 17,很多老项目会直接编译不过,最典型的是Tomcat 10以上把javax.servlet包名换成了jakarta.servlet,老代码里所有import javax.servlet都要改,工作量不小。

我一般建议用JDK 8 + Tomcat 8.5 + MySQL 8.0组合,兼容性最好,网上遇到的问题答案也最容易搜到。IDEA社区版完全够用,Maven看源码里带不带pom.xml,带了就用IDEA自动解析,没带就说明是传统手动导jar包方式。

3.2 Maven项目的依赖引入与核心pom配置

课程设计源码里十有八九是SSM框架(Spring+SpringMVC+MyBatis)或者纯Servlet+JSP两种形态。这里按纯Servlet+JSP加Maven工程为例,因为这是大多数源码包的默认结构,也是你能最快看清楚每一行逻辑的形式。pom.xml核心依赖就三块:

<dependencies> <!-- Servlet API,compile时用,打包时由Tomcat提供 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</scope> </dependency> <!-- JSTL标签库,JSP页面里做循环和格式化 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- MySQL JDBC驱动,版本号和数据库版本对应 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> </dependencies>

依赖里有一个细节:javax.servlet-api的scope是provided,意思是编译时需要、但打包进war时要排除,因为Tomcat自己带了一份servlet实现,重复打包会导致启动时类冲突。mysql-connector-java如果连的是MySQL 5.7,用5.1.49版本更稳定,8.0.30版本也能向下兼容,但驱动类名要从com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver。

导入步骤三步走:IDEA里面File -> Open选中源码目录或pom.xml文件,等待右侧Maven面板下载依赖;如果依赖列表里有红色波浪线,右键pom.xml -> Maven -> Reimport;然后File -> Project Structure确认Project SDK选了1.8,Modules里的Language level也改成8。传统非Maven源码包会带一个lib目录,里面是mysql-connector.jar和jstl.jar,右键lib目录选择Add as Library就能编译。

3.3 数据库连接配置:DBUtil类和三个必填参数

数据库连接在纯Servlet工程里一般封装成一个DBUtil类,静态代码块里注册驱动,getConnection()返回连接。这里给一个适配MySQL 8的通用版本:

package com.supermarket.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/supermarket" + "?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }

连接串里有三个参数必须解释清楚。serverTimezone=Asia/Shanghai是MySQL 8驱动新增的要求,不指定会直接报“The server time zone value is unrecognized”的错,改成北京时间后时区问题消失;characterEncoding=utf8解决从Java到数据库的中文传输问题;useSSL=false关闭SSL握手,本地开发连接更快,报错更少。USER和PASSWORD要改成你自己MySQL账号的实际值,这一步是安装后第一个要动的地方。

3.4 配置Tomcat并启动的四个细节

IDEA里配置Tomcat的顺序是:Run菜单 -> Edit Configurations -> 左上角加号 -> Tomcat Server Local,然后在Deployment页签点加号选择Artifact,选war exploded格式,Application context填/supermarket。第一次启动前确认四件事:Tomcat路径已经指定到本地解压目录;JRE选了项目用的JDK 8;Artifact里依赖了lib;MySQL服务已经启动。

启动后浏览器访问http://localhost:8080/supermarket/login.jsp,看到登录页说明环境通了。如果页面报404,优先看IDEA控制台里Tomcat实际部署的context-path是什么,很多404不是代码问题,而是Application context和你访问的路径不一致。如果启动直接失败,看下面第5章的端口占用排查。

4. 从登录到下单:订单系统的核心代码与事务逻辑

4.1 登录认证:Servlet加Session加过滤器三层写法

登录逻辑在课程设计里有两派写法:要么全部写在单个LoginServlet里,要么更严谨的在过滤器里统一拦截未登录请求。评分差距在这里特别明显——前者每个页面里if一次session判断,代码大量重复;后者用过滤器统一拦,更像企业里的做法。先看LoginServlet的核心写法:

package com.supermarket.web; import com.supermarket.dao.UserDao; import com.supermarket.entity.User; import com.supermarket.util.MD5Util; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; @WebServlet("/login") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = MD5Util.md5(req.getParameter("password")); User user = userDao.findByUsernameAndPassword(username, password); if (user == null) { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } HttpSession session = req.getSession(); session.setAttribute("user", user); resp.sendRedirect(req.getContextPath() + "/index.jsp"); } }

这段代码两个细节值得注意:密码在前端传入后立即做MD5摘要,拿摘要去和数据库比对,即使SQL日志被打出来也不会泄露明文;登录失败用forward回登录页并携带errorMsg,登录成功用sendRedirect跳首页,这样用户按F5刷新时不会重复提交表单。@WebServlet("/login")注解替代了web.xml里的servlet映射配置,省事但要注意不同Tomcat版本对注解扫描的兼容性。

有了登录逻辑还不够,如果用户绕过登录页直接访问/product/list这种路径,系统就形同虚设了。过滤器拦截是这个项目能否拿高分的关键:

package com.supermarket.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import javax.servlet.http.*; import java.io.IOException; @WebFilter("/*") public class LoginFilter implements Filter { @Override 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().substring(request.getContextPath().length()); // 白名单:登录页、登录接口、静态资源不做拦截 if ("/login.jsp".equals(uri) || "/login".equals(uri) || uri.startsWith("/static/")) { chain.doFilter(request, response); return; } if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }

过滤器的拦截路径是/*,也就是所有请求先进过滤器;但登录页本身、登录接口、CSS和JS静态资源要放行,否则会形成“无法登录因为登录页被拦截”的死循环。request.getSession(false)传false表示如果没有Session就返回null而不是新建一个,这是防止未登录用户每次请求都被创建一个无意义的Session对象。评分老师最喜欢演示的漏洞就是“未登录直接访问商品列表”,加了过滤器后这层防护才补齐。

4.2 商品管理:DAO层增删改查的JDBC写法

商品管理是标准的CRUD,分层是JSP -> ProductServlet -> ProductDao -> product表。DAO层列表查询的核心代码最能说明问题:

package com.supermarket.dao; import com.supermarket.entity.Product; import com.supermarket.util.DBUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class ProductDao { public List<Product> findAll(String keyword) { List<Product> list = new ArrayList<>(); String sql = "SELECT id, name, price, stock, unit, supplier_id, status FROM product"; if (keyword != null && !keyword.trim().isEmpty()) { sql += " WHERE name LIKE ?"; } sql += " ORDER BY id"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { if (keyword != null && !keyword.trim().isEmpty()) { ps.setString(1, "%" + keyword.trim() + "%"); } 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")); p.setUnit(rs.getString("unit")); p.setSupplierId(rs.getInt("supplier_id")); p.setStatus(rs.getInt("status")); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; } }

这段JDBC代码有几个必须养成的习惯:用PreparedStatement而不是Statement,参数通过setString(1, "%" + keyword + "%")传入,既能防SQL注入,又让SQL结构清晰;try-with-resources语法自动关闭Connection和PreparedStatement,不用写finally块手动close;price字段用getBigDecimal("price")读取,和数据库的DECIMAL类型对应,用getString再转会丢失精度。Keyword模糊查询是商品模块最常见的加分需求,这里通过动态拼接SQL实现,注意拼接时空格不能少,WHERE前面要有空格,否则SQL语法报错。

新增和更新走同一套路:先写INSERT INTO product(name, price, stock, unit, supplier_id) VALUES(?,?,?,?,?),再逐个set参数;更新是UPDATE product SET name=?, price=?, stock=?, unit=?, supplier_id=?, status=? WHERE id=?。删除更简单,但要注意主表和明细表如果有外键关联,删除商品前要先确认这个商品没有历史订单明细引用,否则会违反外键约束直接报错。

4.3 订单创建:事务保护与扣库存的闭环

订单模块是整个系统用来拉开分数差距的核心。创建订单至少要完成三件事:往orders主表插一条记录、往order_item明细表插多条记录、扣减product表的库存。这三件事必须放在同一个事务里,任何一步失败都要整体回滚。先给订单DAO层的核心代码:

package com.supermarket.dao; import java.math.BigDecimal; import java.sql.*; import java.util.List; public class OrderDao { public String createOrder(Connection conn, int userId, List<CartItem> items) throws SQLException { String orderNo = "SO" + System.currentTimeMillis(); BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { total = total.add(item.getPrice() .multiply(BigDecimal.valueOf(item.getQuantity()))); } // 1. 插入订单主表,获取自增主键 String insertOrder = "INSERT INTO orders(order_no, user_id, total_amount) VALUES(?,?,?)"; try (PreparedStatement psOrder = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS)) { psOrder.setString(1, orderNo); psOrder.setInt(2, userId); psOrder.setBigDecimal(3, total); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); if (keys.next()) { int orderId = keys.getInt(1); // 2. 插入明细 + 扣减库存 insertItemsAndUpdateStock(conn, orderId, items); } } conn.commit(); return orderNo; } private void insertItemsAndUpdateStock(Connection conn, int orderId, List<CartItem> items) throws SQLException { String insertItem = "INSERT INTO order_item(order_id, product_id, quantity, price) VALUES(?,?,?,?)"; String updateStock = "UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?"; for (CartItem item : items) { try (PreparedStatement psItem = conn.prepareStatement(insertItem); PreparedStatement psStock = conn.prepareStatement(updateStock)) { psItem.setInt(1, orderId); psItem.setInt(2, item.getProductId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getPrice()); psItem.executeUpdate(); psStock.setInt(1, item.getQuantity()); psStock.setInt(2, item.getProductId()); psStock.setInt(3, item.getQuantity()); int rows = psStock.executeUpdate(); if (rows == 0) { throw new SQLException("库存不足,订单创建失败"); } } } } }

这段代码有三个设计点。第一,order_no用“SO”加当前毫秒数拼接,简单且不会重复,课程设计不用像企业系统那样接Redis发号器。第二,扣库存的UPDATE语句里带了AND stock >= ?条件,数据库在更新这一行时会自动检查,库存不够时受影响行数是0,直接抛异常触发回滚,这比先SELECT查库存再判断要可靠,因为两条语句之间可能有其它请求修改了库存。第三,Statement.RETURN_GENERATED_KEYS是拿到自增主键的标准做法,订单主表和明细表靠这个返回值建立关联。

事务开关在调用方控制,顺序是:从DBUtil拿Connection、setAutoCommit(false)、执行订单创建、Commit;任何异常滚回:

Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); String orderNo = orderDao.createOrder(conn, userId, cartItems); conn.commit(); } catch (SQLException e) { conn.rollback(); throw new RuntimeException("下单失败,请稍后重试", e); } finally { conn.setAutoCommit(true); conn.close(); }

这里最容易翻车的点是createOrder方法内部拿了一个新连接而不是用调用方传入的conn。每个DAO方法单独DBUtil.getConnection()会导致事务完全失效——订单表插入了,库存异常时回滚的只是最后一条明细,前面的操作全部永久保存。踩过这个坑之后我的习惯是:凡是涉及多表更新的方法,连接一律从参数传进来,绝不在DAO内部自己开新连接。

5. 运行订单系统必踩的5个坑:从数据库连接到404排查

5.1 数据库连接失败的两种典型报错

现象一:启动Tomcat后页面报java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)。原因非常直白:DBUtil里的用户名或密码和本地MySQL实际账号不一致,或者MySQL 8的root账号默认用了新的caching_sha2_password认证插件,旧版驱动不认识。解决方法是先用命令行登录mysql -u root -p验证账号密码,然后看pom里mysql-connector版本,如果是8.0驱动还报错,执行ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码';切换认证插件。

现象二:Communications link failure或Connection refused。这条通常是MySQL服务压根没启动,或者端口被改成了非3306。Windows下按Win+R输入services.msc找到MySQL服务手动启动,Mac下用brew services start mysql启动;确认端口改过的,DBUtil的URL里要把端口一起改掉,比如jdbc:mysql://localhost:3307/supermarket。

5.2 MySQL 8时区报错

现象:启动时报The server time zone value '???ú??±??' is unrecognized or represents more than one time zone,中文环境下这个报错信息经常是乱码。

原因:MySQL 8驱动对时区配置敏感,连接串里没指定serverTimezone参数,驱动拿不到服务器和客户端之间的时区差。

解决:在JDBC URL末尾拼接serverTimezone=Asia/Shanghai&useSSL=false。顺带提醒,这条参数同时解决了页面时间和数据库时间对不上的问题——如果不加,Java里new Date()插入的时间可能和数据库本地时间相差8小时,答辩时显示时间对不上会很尴尬。

5.3 Tomcat启动失败:端口8080被占用

现象:IDEA控制台报Error running 'Tomcat 8.5': Address already in use: JVM_Bind,点击运行直接红色报错。

原因:上一次Tomcat没有正常关闭,进程还占着8080端口,或者电脑上有其它程序用了8080(比如某个调试中的前端项目)。

解决:Windows命令行执行netstat -ano | findstr 8080,看最后一列PID;再执行taskkill /PID 进程号 /F强制杀掉。如果不想杀进程,IDEA里Edit Configurations找到Tomcat Server,把HTTP port改成8081或8082,注意改端口时要连Server port一起改,这个端口是Tomcat内部管理端口,不顺手改的话第二次启动会报“Port already in use”的另一个错误。

5.4 中文乱码的三重门

现象:登录页输入中文后保存到数据库变成问号???,或者从数据库读出来的中文在页面上显示乱码。

原因:编码链路有三个环节,任何一个环节不是UTF-8都会出问题。最常见的是连接串没加characterEncoding=utf8,MySQL建库时字符集不是utf8mb4,或JSP页面没写<%@ page contentType="text/html;charset=UTF-8" %>。

解决:三个地方统一。建库SQL里已经写了DEFAULT CHARACTER SET utf8mb4,这是第一道关;JDBC URL加characterEncoding=utf8是第二道关;第三道关是加一个最基础的编码过滤器,在Web容器层面把所有请求和响应的编码统一:

package com.supermarket.filter; import javax.servlet.*; import javax.servlet.annotation.WebFilter; import java.io.IOException; @WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setCharacterEncoding("UTF-8"); chain.doFilter(req, resp); } }

注意这个过滤器要放在业务过滤器之前执行,它的作用是改变请求体的字符集,让后面Servlet里req.getParameter()读到的就是正确的中文。Tomcat 8及以上版本对GET请求的URI参数已经默认按UTF-8解码,但POST请求体必须靠这个过滤器,写不写效果差别很大。

5.5 部署后404:路径、Artifact和Context三方面排查

现象:首页登录页能打开,点击某个功能后404,或者直接访问/product/list路径报404 Not Found,Tomcat控制台没有异常堆栈。

原因:三类常见情况——页面文件路径和Servlet注解路径对不上;IDEA的Artifact没把依赖jar包含进去,代码编译不过但页面能打开;Application context配置和访问URL前缀不一致。

解决:先在浏览器地址栏观察URL,把http://localhost:8080/supermarket/product/list拆开看,supermarket是context-path,/product/list是Servlet的@WebServlet注解值。IDEA里Deployment页签改动Application context为/supermarket后,重启Tomcat再访问。如果确认路径都对还是404,看IDEA控制台有没有ClassNotFound或NoClassDefFound异常,有的话到Project Structure -> Artifacts -> 选中项目 -> Available Elements里把lib目录拖到Output Layout里。我见过的最隐蔽案例是Maven依赖下载不完整,jar包损坏导致编译产物缺失,Reimport后就好了。

6. 把分数从80拉到95:事务演示、代码规范与验证清单

6.1 用一次现场演示把事务分数拿稳

评分老师最爱问的问题是“下单时如果库存不够怎么办”。口头回答“我有判断”基本拿不到加分,有效做法是现场演示:在订单创建的Service方法里,扣完一个商品的库存后手动抛一个new RuntimeException("模拟异常"),然后访问页面下单,看数据库里订单主表、明细表和库存三个表是否都保持原样。如果事务控制到位,三张表都不变;如果只有订单插入了但库存没扣,说明事务没生效,这是答辩现场最容易翻车的场景。

这个演示的本质是让老师看到你理解“事务要么全部成功要么全部回滚”这件事。演示完记得把模拟异常的代码删掉。另外一个隐藏得分点是conn.setAutoCommit(false)和createOrder方法共用一个连接的问题,老师可能会追问DAO里能不能重新拿连接,答案是不能,重新拿连接事务就断了。

6.2 代码分层和命名规范:老师看得到的“审美分”

95分以上的课程设计,代码的可读性占比很高。包名按功能分层是基本要求:entity里放User、Product、Order实体;dao里放数据库操作;service里放业务逻辑;web里放Servlet;util里放MD5Util、DBUtil这些工具类;filter里放拦截器。如果所有类堆在默认包,光这一个问题就会被扣分。

命名规范对照一下:ProductDao比ProductDap好,findByUsernameAndPassword比selectUser更明确,createOrder比orderAdd更贴近业务语言。这不是玄学,是评审老师扫代码时最快能判断你是否有工程素养的地方。

6.3 答辩前跑一遍验证清单

给老师演示前,按课程设计要求自测一遍功能,我把验证动作和预期结果整理成表,照着走一遍基本稳过:

验证动作预期结果
正确账号密码登录跳转首页,右上角显示用户名
错误密码登录停留登录页,提示“用户名或密码错误”
未登录直接访问商品列表被过滤器拦截,跳回登录页
新增商品,价格填12.345价格显示12.35,数据库存储两位小数
创建含3个商品的订单订单总金额正确,库存相应扣减
把商品库存改成1,一次买2件下单失败,库存和订单都无变化
退出登录后按浏览器后退不能进入功能页,被过滤器拦截

这套验证清单能覆盖评分标准里的功能完整性、安全性和事务正确性。每次带课程设计,我都让同学在答辩现场演示订单失败回滚这个动作,做过这个演示的基本都在90分以上。这个项目就是这样——功能大家都能跑通,差距全藏在细节里。希望这份拆解能帮你把源码跑顺,也能在老师问倒之前想明白为什么要这样建表、为什么订单要拆两张表、为什么扣库存必须跟事务绑在一起。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/5 9:36:09

Open-Shell详解:从安装配置到美化排障,找回经典开始菜单

看到“Open-Shell”这个词&#xff0c;不少老折腾党应该会心一笑——这不就是当年Windows 8/10时代装系统后的必备神器嘛。它的前身叫Classic Shell&#xff0c;后来开源社区接手改名为Open-Shell。别管你是因为讨厌Win11那个居中开始按钮&#xff0c;还是因为Win10磁贴用不顺手…

作者头像 李华
网站建设 2026/10/5 9:35:35

OpenVINO加速YOLOv8s:Linux C++物体检测Demo全解析

简介&#xff1a;面向Linux环境下使用C进行边缘侧视觉推理的开发者&#xff0c;这份Demo演示了如何借助Intel OpenVINO工具包加载并运行YOLOv8s模型完成物体检测。内容覆盖OpenVINO的Model Optimizer与Inference Engine核心组件、IR文件结构中XML与BIN的含义&#xff0c;并展示…

作者头像 李华
网站建设 2026/10/5 9:35:19

51单片机电力载波应用开发(六)数据链

/******************** 鑫盛电子工作室 ********************* 文件名 &#xff1a;main.c* 描述 &#xff1a;通过串口调试软件&#xff0c;向板子发送数据&#xff0c;板子接收到数据后&#xff0c;立即回传给电脑。 * 实验平台&#xff1a;MINI STM32开发板 基于…

作者头像 李华
网站建设 2026/10/5 9:32:52

Nuclei 实战入门:从 YAML 模板到同类型漏洞的批量排查

一、工具定位&#xff1a;为什么是 Nuclei&#xff1f;它是一款基于 YAML 语法模板的定制化快速漏洞扫描器&#xff0c;用 Go 语言开发&#xff0c;核心优势在于可配置、可扩展、易用。简单来说&#xff0c;它的价值在于把“某处漏洞的验证逻辑”变成了一个可以共享、复用、批量…

作者头像 李华
网站建设 2026/10/5 9:32:31

STM32解析GPS模块NMEA0183数据:从串口字节流到经纬度转换完整指南

拿到STM32和GPS模块&#xff0c;第一件事就是接上串口看输出。如果你用USB转TTL直接接GPS模块&#xff0c;打开串口助手&#xff0c;会看到一行行以$开头的字符串&#xff0c;比如$GPRMC,083559.00,A,3958.7888,N,11625.5624,E,0.047,66.05,080724,,,D*68。很多人到这一步就开始…

作者头像 李华
网站建设 2026/10/5 9:32:28

AI编程工作流实战:从提示词到代码生成、调试与测试的完整落地

最近后台私信里频率最高的一个问题&#xff0c;就是“AI 编程到底怎么落地”。大家普遍的感觉是&#xff1a;让大模型写个简单函数挺快的&#xff0c;但一放到真实项目里就变味了——要么上下文太长&#xff0c;要么生成的代码风格和项目完全对不上&#xff0c;要么改三遍还带着…

作者头像 李华