ARTICLE DETAIL

资讯详情

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

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

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. 数据库建模先行超市订单系统的表结构与建表SQL2.1 功能模块怎么拆从登录到订单的完整链路先别急着看代码把需求落成数据模型再动手后面基本不返工。超市订单管理系统最常见的需求拆法是四个模块加一块用户模块负责管理员登录和密码修改简单版就一张用户表登录页一个ServletSession里放一个User对象商品模块负责商品的增删改查、名称模糊查询和上下架状态供应商模块负责供应商的信息维护和商品形成一对多关系订单模块负责选商品、填数量、提交订单、生成订单主表和明细、同时扣减库存。这四个模块里订单模块是整个系统最考验数据库功底的部分也是评分老师最常追问的地方。订单包含多个商品天然是一对多结构要把订单拆成主表和明细表两张才能完整表达。用户下单产生订单订单包含多条订单明细每条明细指向一个商品商品归属一个供应商——实体关系一句话讲完外键只加在订单明细和商品这一层不往下继续级联否则删数据会引发一串连锁反应。2.2 六张核心表的建表SQL与字段设计理由直接给建表语句。这里有个实际操作建议把整个建库建表过程录屏答辩时放给老师看直接证明这份设计是你亲手做过而不是从别人zip里解压出来的。-- 建库字符集统一用 utf8mb4MySQL 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 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 备注 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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.0MySQL 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框架SpringSpringMVCMyBatis或者纯ServletJSP两种形态。这里按纯ServletJSP加Maven工程为例因为这是大多数源码包的默认结构也是你能最快看清楚每一行逻辑的形式。pom.xml核心依赖就三块dependencies !-- Servlet APIcompile时用打包时由Tomcat提供 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSTL标签库JSP页面里做循环和格式化 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL JDBC驱动版本号和数据库版本对应 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.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.8Modules里的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 ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; 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); } }连接串里有三个参数必须解释清楚。serverTimezoneAsia/Shanghai是MySQL 8驱动新增的要求不指定会直接报“The server time zone value is unrecognized”的错改成北京时间后时区问题消失characterEncodingutf8解决从Java到数据库的中文传输问题useSSLfalse关闭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 8Artifact里依赖了libMySQL服务已经启动。启动后浏览器访问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 ListProduct findAll(String keyword) { ListProduct 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块手动closeprice字段用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, ListCartItem 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, ListCartItem 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 rootlocalhost (using password: YES)。原因非常直白DBUtil里的用户名或密码和本地MySQL实际账号不一致或者MySQL 8的root账号默认用了新的caching_sha2_password认证插件旧版驱动不认识。解决方法是先用命令行登录mysql -u root -p验证账号密码然后看pom里mysql-connector版本如果是8.0驱动还报错执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;切换认证插件。现象二Communications link failure或Connection refused。这条通常是MySQL服务压根没启动或者端口被改成了非3306。Windows下按WinR输入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末尾拼接serverTimezoneAsia/ShanghaiuseSSLfalse。顺带提醒这条参数同时解决了页面时间和数据库时间对不上的问题——如果不加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都会出问题。最常见的是连接串没加characterEncodingutf8MySQL建库时字符集不是utf8mb4或JSP页面没写% page contentTypetext/html;charsetUTF-8 %。解决三个地方统一。建库SQL里已经写了DEFAULT CHARACTER SET utf8mb4这是第一道关JDBC URL加characterEncodingutf8是第二道关第三道关是加一个最基础的编码过滤器在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 FoundTomcat控制台没有异常堆栈。原因三类常见情况——页面文件路径和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里放Servletutil里放MD5Util、DBUtil这些工具类filter里放拦截器。如果所有类堆在默认包光这一个问题就会被扣分。命名规范对照一下ProductDao比ProductDap好findByUsernameAndPassword比selectUser更明确createOrder比orderAdd更贴近业务语言。这不是玄学是评审老师扫代码时最快能判断你是否有工程素养的地方。6.3 答辩前跑一遍验证清单给老师演示前按课程设计要求自测一遍功能我把验证动作和预期结果整理成表照着走一遍基本稳过验证动作预期结果正确账号密码登录跳转首页右上角显示用户名错误密码登录停留登录页提示“用户名或密码错误”未登录直接访问商品列表被过滤器拦截跳回登录页新增商品价格填12.345价格显示12.35数据库存储两位小数创建含3个商品的订单订单总金额正确库存相应扣减把商品库存改成1一次买2件下单失败库存和订单都无变化退出登录后按浏览器后退不能进入功能页被过滤器拦截这套验证清单能覆盖评分标准里的功能完整性、安全性和事务正确性。每次带课程设计我都让同学在答辩现场演示订单失败回滚这个动作做过这个演示的基本都在90分以上。这个项目就是这样——功能大家都能跑通差距全藏在细节里。希望这份拆解能帮你把源码跑顺也能在老师问倒之前想明白为什么要这样建表、为什么订单要拆两张表、为什么扣库存必须跟事务绑在一起。本文还有配套的精品资源点击获取
返回列表