简介基于 Java Web 的超市管理系统毕业设计项目内含完整源码、数据库脚本和运行配置面向计算机、通信、人工智能、自动化等专业的学生、老师及从业者。项目经过调试与测试答辩评审分达 98 分整体可作为期末课程设计、课程大作业或毕业设计的直接参考。压缩包共 386 个文件大小 10.36MB其中 49 个 Java 源文件和 56 个 JSP 页面构成主要业务逻辑与页面层98 个 class 文件为编译后字节码可直接部署运行SQL 脚本可快速导入建库另有 XML 配置、CSS/LESS/SCSS 样式、JavaScript 脚本及图片等资源覆盖前端展示与后端处理所需内容。项目中控制器、DAO 实现类与 JSP 页面分层明确可快速定位商品、会员、供应商、结账等核心功能入口。已有 156 人学习使用适合作为入门到进阶的实践素材基础较好的读者可在现有功能模块上继续扩展也可以按自身需求调整业务逻辑具有较强的二次开发价值。1. 这个 javaweb 超市管理系统到底在做什么值不值得选每年毕业季总有人从各种渠道下载到基于 javaweb 的超市管理系统这类项目解压后对着十几个文件夹和一坨 XML 配置发呆要么数据库连不上要么页面 404最后一边骂怎么跑不起来一边又找下一个源码包。实际上这个题目是 javaweb 方向最稳妥的毕业设计选题之一它不追求新框架而是把 JSP、Servlet、MySQL、JDBC 这一套经典技术栈完整串起来做成一个业务闭环完整的系统。你做完它既能在答辩时把每一行代码讲明白又能在简历上写独立完成进销存核心模块设计与实现。如果你正在纠结选题或者已经在做但卡在某个环节这篇文章把从建库到部署的完整方案拆开讲。这个系统的价值不在于超市这个业务本身而在于它覆盖了 javaweb 项目最常见的三类核心需求登录权限控制、商品进销存数据流转、报表统计。这些能力换个壳放到图书管理、药店管理、仓库管理系统里完全通用。它的技术栈旧但不过时——你去看很多公司的老旧维护项目还在用 JSPServlet 的并不少见。适合的人群很明确JavaWeb 刚学完、需要巩固综合能力的学生以及想快速搭建一个能演示、能答辩、能写进简历的项目的人。2. 选型与数据库设计为什么用 JSPServlet 而不是 Spring Boot2.1 技术选型毕业设计场景下 jspservlet 依旧是稳妥方案很多人会问现在企业里都用 Spring Boot为什么毕业设计还选 Servlet这不是技术落后而是答辩逻辑决定的。毕业设计评委会问你连接池怎么做请求怎么分发事务怎么控制如果你用 Spring Boot这些都被框架封住了你说不出底层实现只能答框架帮我处理了这在本科答辩里很吃亏。而 JSPServlet 这套组合你能明确说出浏览器发请求到 TomcatServlet 接收请求调用业务层业务层通过 JDBC 操作 MySQL数据渲染到 JSP 页面返回。这条链路每一个环节都是你自己的代码评委看得见你的工作量。常见做法是三层的 MVC 结构JSP 负责页面展示Servlet 负责接收请求和控制跳转ServiceDAO 负责业务逻辑和数据库操作。有些同学会把业务全写在 Servlet 里页面里嵌大量 Java 代码系统照样能跑但数据库连接不关、SQL 拼字符串这些问题会在答辩时被追问得很痛苦。我一般建议用 Druid 连接池配合 JdbcTemplate 或者原生 JDBC 封装既避免手写连接管理的低级错误又保留了 SQL 的可见性。2.2 建库建表八张表拆解超市业务的最小闭环数据库是标题里含数据库的分量所在表结构设计得好不好答辩时一眼就能看出来。超市管理系统最少需要八张表用户表、商品分类表、商品表、供应商表、进货单表、进货明细表、销售单表、销售明细表。用户表管登录分类表管商品的层级归类商品表是核心进货和销售通过单据表加明细表的方式记录这样每一笔业务都有据可查。先创建数据库再按依赖顺序建表以下是核心建表脚本MySQL 5.7 及以上均可运行CREATE DATABASE IF NOT EXISTS supermarket_db DEFAULT CHARACTER SET utf8mb4; USE supermarket_db; -- 用户表区分管理员、收银员、仓管员 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 存储MD5加密后密文, real_name VARCHAR(32) COMMENT 姓名, role TINYINT NOT NULL DEFAULT 1 COMMENT 1管理员 2收银员 3仓管员, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT用户表; -- 商品表中库存字段承担关键预警职责 CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL COMMENT 关联分类表, product_code VARCHAR(32) NOT NULL UNIQUE COMMENT 商品编码扫码枪扫的就是它, product_name VARCHAR(64) NOT NULL, spec VARCHAR(64) COMMENT 规格如500ml/瓶, unit VARCHAR(8) NOT NULL COMMENT 单位瓶/包/盒, purchase_price DECIMAL(10,2) NOT NULL COMMENT 进价成本计算依据, sale_price DECIMAL(10,2) NOT NULL COMMENT 售价, stock INT NOT NULL DEFAULT 0 COMMENT 当前库存, stock_min INT NOT NULL DEFAULT 10 COMMENT 库存预警下限, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT商品表;这段 SQL 里最值得注意的设计决策是库存字段直接冗余在商品表里。有些教程会把库存设计成每次进货和销售时去单据明细表里 SUM 计算得到理论上更规范但实际业务里每次查询商品都要扫描所有明细数据库压力和代码复杂度都不合算。在超市这种高频读商品的场景下冗余一个 stock 字段让查询变成一次主键读取配合事务控制来保证不超卖是工程上更务实的做法。商品编码 product_code 建议用纯数字条码格式后面对接扫码枪输入时不需要改代码。供应商表、进货单表和明细表的结构同样要注意关联方式。进货单主表只存供应商、操作员、总金额、进货时间每一条商品进入进库明细表。这种主表明细表的设计在销售单上同样适用是进销存系统的通用范式。下面把这三张表的建表语句补全CREATE TABLE t_supplier ( id INT PRIMARY KEY AUTO_INCREMENT, supplier_name VARCHAR(64) NOT NULL, contact VARCHAR(32) COMMENT 联系人, phone VARCHAR(20), address VARCHAR(128) ) ENGINEInnoDB COMMENT供应商表; -- 进货单主表一笔进货对应一条记录 CREATE TABLE t_stock_in ( id INT PRIMARY KEY AUTO_INCREMENT, supplier_id INT NOT NULL, user_id INT NOT NULL COMMENT 操作员, total_amount DECIMAL(10,2) NOT NULL COMMENT 进货总金额冗余存储便于统计, stock_in_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT进货单主表; -- 进货明细一条主表记录对应多行商品 CREATE TABLE t_stock_in_item ( id INT PRIMARY KEY AUTO_INCREMENT, stock_in_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL COMMENT 进货数量, price DECIMAL(10,2) NOT NULL COMMENT 当时进价商品改价不影响历史单据 ) ENGINEInnoDB COMMENT进货明细表;明细表里冗余 price 字段是一个关键设计如果商品进价变动历史单据里的价格仍然保持录入时的值不会跟着商品表一起变。这在做利润统计和审计时非常重要。另一个关键点是金额的冗余存储进货单主表里的 total_amount 是通过明细算完后写进去的统计某月进货总额直接 SUM 主表即可不用再 JOIN 明细表这是典型的以空间换时间的做法。2.3 把 SQL 导入数据库并验证表结构建表语句准备好后启动 MySQL 服务在命令行或者 Navicat 里执行脚本。如果你用的是 MySQL 8.0 以上版本要注意驱动和连接串的差异这在后面连接池配置时会详细讲。导入后执行SHOW TABLES;应该能看到全部八张表再执行DESC t_product;检查字段类型确认utf8mb4字符集没有乱码风险。创建一个测试用户并插入一条商品数据方便后面联调登录和商品查询功能INSERT INTO t_user(username, password, real_name, role) VALUES(admin, MD5(123456), 系统管理员, 1); INSERT INTO t_product(category_id, product_code, product_name, unit, purchase_price, sale_price, stock, stock_min) VALUES(1, 6901234567890, 农夫山泉550ml, 瓶, 1.20, 2.00, 100, 20);这里插入的是 MD5 加密后的密码实际开发中更好的做法是 MD5 加盐但毕业设计用 MD5 已经足够描述清楚你的加密方案。注意建表顺序——先建用户表和分类表再建商品表因为商品表的外键依赖分类表如果顺序反过来会报 1215 错误无法添加外键约束。3. 搭建 javaweb 项目骨架Maven 结构、连接池和第一个 Servlet3.1 用 IDEA 新建项目并引入依赖现在开始建工程。这里以 IDEA 2022 及以上版本为例新建项目时选择 Maven 的 webapp 模板或者直接建一个普通 Maven 项目再右键添加框架支持—Web两种方式都可以。项目结构上src/main/java放 Java 代码src/main/resources放配置文件和 SQL 脚本src/main/webapp放 JSP 页面和静态资源。这里要提醒一下很多毕业设计源码包是从 Eclipse 导出的目录结构是 src/webroot拿到 IDEA 里要手动调整否则部署时找不到 JSP 文件。我见过太多人卡在这步。在pom.xml里引入关键依赖版本要匹配你本机的环境和 Tomcat 版本。下面是核心依赖配置dependencies !-- Servlet APITomcat 9 对应 javax.servlet-api 4.0.1 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- JSTL 标签库 -- dependency groupIdjstl/groupId artifactIdjstl/artifactId version1.2/version /dependency !-- MySQL 驱动按本地数据库版本选择8.0.33 兼容 5.7 和 8.0 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- Druid 连接池 -- dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency /dependenciesscope为 provided 的依赖在打包时不会打进去因为 Tomcat 自带 Servl et 和 JSP 的 API 实现重复打入反而会报冲突。如果本地 MySQL 是 5.6 或 5.7也可以把连接驱动换成mysql-connector-java 5.1.49对应的driverClassName也要同步调整。Druid 是目前国内中小项目里最常用的连接池提供监控页面和 SQL 防注入功能答辩时可以多讲它它不只是连接复用还能帮你看到每个 SQL 的执行耗时。3.2 编写 db.properties 和 Druid 工具类连接配置单独放在src/main/resources/db.properties里方便切换环境。这里有一个高频踩坑点MySQL 8.0 的连接驱动类名是com.mysql.cj.jdbc.Driver而 5.7 时代是com.mysql.jdbc.Driver抄旧源码的人经常在这里翻车。另外连接串里要显式指定useUnicodetruecharacterEncodingutf8和serverTimezone否则插入中文数据会乱码高版本 MySQL 还会直接报时区错误。driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://127.0.0.1:3306/supermarket_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai usernameroot password你自己的密码 # 初始连接数、最大连接数、最大等待时间 initialSize5 maxActive20 maxWait60000在java目录下建com.supermarket.util.DBUtil类。这个工具类全局只创建一个 DruidDataSource 实例所有 DAO 层代码都通过它拿连接和释放资源。注意这里必须用静态代码块保证只初始化一次否则每次请求都新建连接池会被系统资源直接拖垮package com.supermarket.util; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.ResultSet; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static DataSource dataSource; static { try { InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties); Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败 e.getMessage()); } } public static Connection getConnection() throws Exception { return dataSource.getConnection(); } /** 数据库连接池中拿到的连接调用 close() 只是归还连接 */ public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (Exception ignored) {} try { if (stmt ! null) stmt.close(); } catch (Exception ignored) {} try { if (conn ! null) conn.close(); } catch (Exception ignored) {} } }代码逻辑上这类工具类是整个系统的地基。任何 DAO 里都通过DBUtil.getConnection()拿连接用完必须调用close()归还。Druid 的close()行为不是真正关闭连接而是归还到池里这一点要在答辩时说清楚——你并不是放弃了连接而是在做连接复用。参数方面maxActive设 20 对毕业设计足够并发量不大的情况下 20 个连接已经富余maxWait设 60000 表示如果池里没有空闲连接最多等待 60 秒超时则抛出异常避免请求无限阻塞。3.3 用 BaseServlet 统一请求分发告别 Servlet 类爆炸如果按传统方式做商品模块要写商品列表 Servlet、添加商品 Servlet、删除商品 Servlet……一个模块三四张 Servlet 类整个系统会膨胀到二十多个类维护起来非常痛苦。这里换一种思路写一个 BaseServlet 子类利用反射根据请求参数里的action值来路由到子类的具体方法。这是一个 show 代码功力明显的设计答辩时讲出来能加分不少。BaseServlet 代码如下package com.supermarket.servlet; import com.supermarket.util.DBUtil; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; /** * 所有业务 Servlet 的父类 * 子类通过继承它自动获得按 action 参数分发请求的能力 */ public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String action req.getParameter(action); if (action null || action.isEmpty()) { resp.sendError(400, 缺少 action 参数); return; } try { // 反射调用子类中与 action 同名的方法 Method method this.getClass().getDeclaredMethod(action, HttpServletRequest.class, HttpServletResponse.class); method.setAccessible(true); method.invoke(this, req, resp); } catch (Exception e) { throw new ServletException(请求分发失败 action, e); } } }逻辑上所有的业务 Servlet 都继承这个 BaseServlet新增加一个功能只需要在当前 Servlet 里多写一个 public 方法方法名就是 action 的值。例如ProductServlet里有list方法和add方法前端请求product?actionlist就会自己走到 list 方法。到了 Servlet 3.0 以后映射关系用 WebServlet 注解写在子类上不用再改 web.xml代码能少写一大截。反射调用的性能损耗可以忽略不计Tomcat 会复用 Servlet 实例。对应的写一个 LoginServlet 演示登录逻辑和 Session 控制WebServlet(/login) public class LoginServlet extends BaseServlet { /** 渲染登录页面 */ public void toLogin(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.getRequestDispatcher(/login.jsp).forward(req, resp); } /** 校验登录 */ public void doLogin(HttpServletRequest req, HttpServletResponse resp) throws Exception { String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); UserDao userDao new UserDao(); User user userDao.findByUsernameAndPassword(username, password); if (user ! null) { // 存入 Session后续用过滤器判断是否已登录 req.getSession().setAttribute(loginUser, user); resp.sendRedirect(req.getContextPath() /product?actionlist); } else { req.setAttribute(errorMsg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }MD5Util 是一个自己封装的工具类对MessageDigest做包装把密码摘要成十六进制字符串。注意这里有个安全习惯数据库里存的是 MD5 密文用户输入密码后要在 Java 端先加密再比较绝不能WHERE password$password直接拼接明文。登录成功后用重定向而不是转发这样可以防止用户刷新页面时重复提交表单。Session 里的 loginUser 会被后面的登录过滤器读取这是 javaweb 项目做权限控制的标准姿势。3.4 登录过滤器和 web.xml 配置让未登录用户无法访问敏感页面登录校验做完必须配上过滤器拦住那些不登录就输入 URL 访问页面的请求。创建一个 LoginFilter 实现javax.servlet.Filter接口在doFilter方法里判断 Session 是否存在用户存在就放行不存在直接重定向到登录页。要注意放行静态资源和登录相关的 URL否则模板引擎一加载 CSS 图片就被拦住了页面会变成纯文本裸奔。filter filter-nameloginFilter/filter-name filter-classcom.supermarket.filter.LoginFilter/filter-class /filter filter-mapping filter-nameloginFilter/filter-name !-- 拦所有请求但静态资源和登录页需要手动放行 -- url-pattern/*/url-pattern /filter-mapping如果项目用的是纯注解方式也可以用WebFilter(/*)但是要注意dispatcher配置在 web.xml 和注解混用时代很容易出现过滤器重复执行。配置完成后启动 Tomcat浏览器访问http://localhost:8080/supermarket/login能看到登录页面说明环境已经通了一半——接下来用测试账号登录并跳到商品列表页这一条链路跑通后面所有模块的排查思路就清晰了。4. 核心业务实现进货入库和 POS 收银的事务与库存联动4.1 进货入库主表明细表的写入逻辑超市系统最核心的闭环是进货 → 入库 → 上架 → 销售 → 库存扣减。进货这一步页面是选择供应商 动态添加商品行提交到后端时请求里会携带一个供应商 ID、多个商品 ID 和数量。这里故意设计成单个 Servlet 的单个方法一次性接收所有参数不能再走常见的逐条保存。具体逻辑拆解如下WebServlet(/stockIn) public class StockInServlet extends BaseServlet { public void add(HttpServletRequest req, HttpServletResponse resp) throws Exception { int supplierId Integer.parseInt(req.getParameter(supplierId)); int userId ((User) req.getSession().getAttribute(loginUser)).getId(); String[] productIds req.getParameterValues(productId); String[] quantities req.getParameterValues(quantity); String[] prices req.getParameterValues(price); // 内部累加总金额同时生成主表记录 double totalAmount 0; for (int i 0; i productIds.length; i) { totalAmount Double.parseDouble(prices[i]) * Integer.parseInt(quantities[i]); } Connection conn null; try { conn DBUtil.getConnection(); // 开启事务之后任何一步失败进库主表都不会留下残缺数据 conn.setAutoCommit(false); StockInDao stockInDao new StockInDao(); StockInItemDao itemDao new StockInItemDao(); ProductDao productDao new ProductDao(); // 1. 插入进货单主表拿到自增 ID int stockInId stockInDao.insert(conn, supplierId, userId, totalAmount); // 2. 逐条插入明细表 for (int i 0; i productIds.length; i) { int productId Integer.parseInt(productIds[i]); int quantity Integer.parseInt(quantities[i]); double price Double.parseDouble(prices[i]); itemDao.insert(conn, stockInId, productId, quantity, price); // 3. 同步累加商品库存 productDao.increaseStock(conn, productId, quantity); } // 全部成功才提交 conn.commit(); resp.sendRedirect(req.getContextPath() /stockIn?actionlist); } catch (Exception e) { // 异常则整体回滚保证数据一致 conn.rollback(); throw new ServletException(进货失败已回滚 e.getMessage(), e); } finally { conn.setAutoCommit(true); DBUtil.close(conn, null, null); } } }这段代码有两个关键点必须讲给读者。第一是conn.setAutoCommit(false)开启手动事务把插主表 插明细 更新库存变成一个原子操作任何一步抛异常所有已执行的操作全部回滚。如果没有事务极容易出现主表写了一条进货单但库存没加上的脏数据。第二是 DAO 方法全部接受Connection参数——这是为了保证它们参与同一个事务而每个方法内部不能再自己DBUtil.getConnection()拿新连接否则事务就断开了。这是很多不成熟的源码最常见的错误把事务相关的代码写得看似有但实际无效。参数说明上productIds使用getParameterValues接收同名参数数组这要求前端表单里每个商品行的name属性都叫productId。如果实际运行时只拿到一条数据先去看浏览器 Network 面板再决定是改前端还是改后端。totalAmount在 Java 端累加后写入主表避免数据库里多次 SUM 带来的性能浪费。库存更新用的increaseStock是 PreparedStatement 的UPDATE t_product SET stock stock ? WHERE id ?这比先 SELECT 再 UPDATE 的读改写方式更安全不会因为并发读取旧值而覆盖掉中间的变动。4.2 收银出库同时扣库存和生成销售单收银台是另一个高频操作。顾客选完商品后收银员在页面逐个扫描条码系统累加购物车最后点击结算后端一次性处理生成销售单主表、写入销售明细、扣减库存。扣库存的 SQL 非常关键——把它设计成条件更新只有stock 购买数量时才执行成功这正是解决超卖的方法UPDATE t_product SET stock stock - ? WHERE id ? AND stock ?前端收银页的核心逻辑是输入框监听回车事件条码匹配后通过 AJAX 查询商品信息把名称价格追加到购物车列表。这个效果听起来复杂但可以用 jQuery 加少量 JavaScript 在 JSP 页面里实现。关键代码逻辑是每扫描一个条码就向product?actionfindByCode发一个请求成功后往表格里追加一行。结算时把购物车数据依次填到隐藏表单字段里一次性提交全部数据。这种设计要说明的是数据量在单次几十件以内时完全没有性能压力不需要把购物车放到 Session 里每次结算都携带完整商品数组即可结构简单也不容易出错。4.3 库存预警让项目看起来会思考进销存之外一个加分模块是库存预警。商品表里的stock_min字段此刻派上用场商品列表页加载时对所有stock stock_min的商品打上红色标记首页再用一个 SQL 查出需要补货的商品数量。这个功能实现难度低、可视性强答辩时评委一般都会看上两眼。典型实现是商品列表 DAO 里加一个方法public ListProduct findLowStockList(Connection conn) { String sql SELECT * FROM t_product WHERE stock stock_min ORDER BY (stock_min - stock) DESC; return queryList(conn, sql); }把低库存列表放到后台首页登录后第一眼就能看到哪些商品需要补货这就是整个系统里最有业务价值的功能之一。从能用到好用库存预警是一个性价比很高的功能点。对比维度上它和销售时段统计报表一样可以让项目在功能列表里多出决策辅助这一分类而后者可以基于销售单据表按小时分组聚合SQL 复杂一点但同样可控。5. 部署和测试避坑5 个你能遇到的痛苦时刻和对应解法5.1 现象Druid 启动直接抛 ClassNotFoundException项目根本起不来这个报错是驱动 JAR 没有真正进入 Tomcat 的 classpath 造成的。原因一般是 Maven 依赖没导入全或者是 IDEA 的 Artifacts 配置里没有把 mysql-connector 这个 jar 加入WEB-INF/lib。很多人只在 pom.xml 里写了依赖就以为万事大吉实际上 IDEA 要把依赖打进去还需要重新构建 Artifact。解决方法是打开File → Project Structure → Artifacts在 Output Layout 里右键Put into Output Root把mysql-connector-java和druid的 jar 加入WEB-INF/lib然后Build → Rebuild Project。如果还是报错用压缩软件打开生成的 war 包检查WEB-INF/lib下有没有这两个 jar这个土办法最直观。5.2 现象插入数据库的中文全是问号或者变成 æ²³å 这类乱码原因通常是三层字符集不一致数据库表字符集、JDBC 连接串字符集、JSP 页面字符集。建库时已经用了utf8mb4连接串里也写入characterEncodingutf8那乱码多半出在 JSP 或者请求编码上。检查所有 JSP 页面开头是否有% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%以及 Tomcat 是否设置了 URI 编码在conf/server.xml的 Connector 节点上加URIEncodingUTF-8。GET 请求的参数默认按 ISO-8859-1 解码这是鄙视链最顶端一个隐藏很深的坑POST 请求则靠req.setCharacterEncoding(UTF-8)解决。如果你每一步都对还是乱码用数据库客户端直接执行 SQL 插入一条中文数据能正常显示的话说明问题确实在应用层。5.3 现象登录后跳转商品列表 404控制台找不到任何错误这种情况大概率不是代码错了而是访问路径不对。Servlet 配置的映射是/product但请求里写成了/productServlet或者 JSP 页面里表单 action 写的路径没有带req.getContextPath()。在 IDEA 内置 Tomcat 中运行时应用上下文路径就是部署时设置的名称比如http://localhost:8080/supermarket_war_exploded/后面这串又臭又长的路径是 IDEA 默认生成的应该手动改成/supermarket。修改方式Edit Configurations → Deployment → Application context改成/supermarket以后所有链接统一以${pageContext.request.contextPath}开头。这一步是新手花时间最多的地方不是代码逻辑问题而是工程配置问题要一步步排查。5.4 现象服务器启动后访问报 500堆栈里提示 Client does not support authentication protocol requested by server这是 MySQL 8.0 特有的坑默认密码加密方式是caching_sha2_password而老版本的 MySQL 驱动 JDBC 不支持。解决方法是把驱动升级到 8.0 以上或者把用户密码认证方式改成mysql_native_password。在数据库里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;如果改了还是报错检查连接串里是否漏加了serverTimezoneAsia/Shanghai高版本 MySQL 驱动要求必须显式指定时区否则连接时会抛 SQLException。这些其实都不是复杂原理就是每个做 javaweb 的人都会走一遍的路说出来是为了让你遇到时不用问人也能自己判断。5.5 现象两个用户同时买同一个商品库存变成负数这属于典型的并发超卖问题。原因在于用先 SELECT stock 再 UPDATE stock stock - 1两步操作两个请求同时读到旧库存就会互相覆盖。解决方案就是前面收银出库部分写的条件更新 SQLUPDATE t_product SET stock stock - ? WHERE id ? AND stock ?让数据库自己去判断扣减条件而不是靠 Java 代码先查后设。这个 SQL 保证了只有当前库存大于等于本次购买数量时才会真正执行更新JDBC 返回的影响行数为 0则说明库存不足本次结算直接提示用户。这种处理在数据库层面保证了原子性也是进销存系统最常被考察的点。6. 答辩前做这两件事数据验证和报表之星系统功能做完别急着写论文先用数据验证证明系统的可靠性。准备一个十行左右的 SQL 脚本模拟多次进货和多次销售然后核对商品表库存是否等于进货总量减销售总量。如果一致说明事务控制正确如果不一致回头检查 4.1 节里的事务边界是否有 Service 层绕过 Connection 参数的问题。这一步的验证结论可以直接写进毕业论文的系统测试章节比你随便写几行测试用例有说服力得多。第二个加分项是做一个简单的近 30 天销售趋势报表页面。SQL 用DATE_FORMAT(sale_time, %Y-%m-%d)做分组COUNT 算单量SUM 算销售额每天一条记录返回。页面放一个表格或者用 ECharts 画个折线图展示波峰波谷日期。这个功能实现的绝对难度不大但它让项目从增删改查进阶到了数据可视化分析答辩时把亮点落在这里评委对工作量的认可度会明显不一样。以前带过的一个学生技术栈完全一样就是多了这个报表答辩成绩从良变成了优。毕业设计项目的价值不完全在于功能数量而是在于你能把每个功能的设计缘由和工程权衡说清楚。希望这些经验能帮你在答辩前把系统打磨得又稳又亮眼如果一路照着搭下来遇到什么反常识的报错多半是环境问题而不是代码问题别急着删代码重来先从配置上找原因。本文还有配套的精品资源点击获取
