
如果你正在为 JavaWeb 课程设计发愁不知道选什么题我建议把目光放在超市收银系统上。这个题目看起来朴素做起来有料答辩又不太容易翻车功能闭环完整有登录权限、有商品录入、有购物车结算、有库存扣减、有销售记录查询数据库表关系也不复杂用一套 Servlet JSP JDBC MySQL 的组合正好能把课本上学的东西全部串起来。我自己当年就是用超市收银系统交掉的课设报告最后还把完整源代码整理成附件一起提交了今天就把整个设计思路、数据库建模、核心代码和踩过的坑掰开揉碎讲一遍。这个内容适合正在做课设的学生抄作业也适合想快速了解 JavaWeb 经典三层架构的初学者参考。你不用把它当成多高深的企业级项目它就是一门课设但要做得像样至少把 MVC 分层、事务处理、SQL 防注入、会话管理这些点都覆盖到。下面我按课设报告的习惯顺序来展开前面讲设计思路和需求分析中间讲核心实现和代码最后是常见问题速查和答辩经验。1. 内容整体设计与思路拆解1.1 为什么选“超市收银系统”当课设课设选题这件事最怕两种一种是题目太小写不满报告也展示不了技术含量另一种是题目太大做到一半发现收不住最后草草交个半成品。超市收银系统恰好踩在中间线。从业务场景看超市收银是一个非常真实、非常日常的场景。顾客拿商品来结账收银员扫码系统识别商品、累加金额、收钱、找零、打出小票顺带还要把库存减掉。这个过程天然包含了“输入 - 处理 - 输出 - 数据落库”的完整逻辑不需要你凭空发明业务规则答辩时老师也容易理解你做了什么。从技术教学角度看这个题目几乎把 JavaWeb 的核心知识点都覆盖了。你至少需要做到登录才能访问系统会话管理、区分管理员和收银员权限模型、商品信息增删改查基础 CRUD、购物车临时数据Session 存取、订单和库存联动数据库事务、销售记录按时间查询SQL 聚合。这些点随便拿出来一个都能在报告里写几百字组合在一起一份课设报告的厚度自然就上去了。从答辩容错率看超市收银系统的功能边界非常清晰。你不需要扯什么高并发分布式那也不是课设讨论的范畴。你只需要把“单人操作一台收银机”这个场景做扎实把事务和防超卖讲明白老师基本就不会追问到你答不上来的地方。1.2 技术栈选型JavaWeb 课设的经典组合我在做这个项目时选的是 Servlet JSP JDBC MySQL Tomcat没有用 Spring Boot也没有用 MyBatis。原因很简单这是课设不是实习项目。Jetty 或者 Tomcat 作为 Web 容器Servlet 负责接收请求和响应JSP 负责渲染页面JDBC 负责直连数据库MySQL 负责存储数据。这套组合的好处是每一层都“看得见、摸得着”。你写一个 Servlet能清楚地看到请求怎么进来、参数怎么解析、业务逻辑怎么调用、结果怎么转发你写一个 JSP能直接在里面写 % % 脚本片段把后端数据展示到表格里。这种原始但清晰的过程正好是课程设计最想考察的东西。当然如果你们课设明确允许使用 Spring Boot那会省很多事但也要小心一个问题Spring Boot 封装太狠很多人写完一个项目连 HTTP 请求是怎么从一个接口走到数据库的都不清楚答辩时老师问一句“ Controller 和 Servlet 什么关系”就卡壳了。用原生 Servlet JSP 虽然代码量大一点但每一步都在你自己的掌控范围内心里有底。前端部分我没有写太复杂的东西直接用 Bootstrap 的 CDN 加速样式加上少量原生 JavaScript。收银台页面用了一个大按钮“添加商品”配合扫码枪自动输入条码和回车事件操作起来很像真实收银台的感觉。没必要为了炫技引入 Vue 或者 React课设重点是后端逻辑前端能看能用就行。1.3 数据库设计与表结构规划超市收银系统的数据库我拆成了四张表用户表、商品表、销售记录主表、销售记录明细表。一开始我也想过把销售记录和明细合成一张表后来实际跑了一遍数据发现这样会非常混乱对账和统计都很难写所以最后还是按标准的一对多关系来设计。先看用户表。这张表不需要复杂字段就是 id、用户名、密码、角色角色区分管理员和收银员。管理员可以管理商品收银员只能收银和查看记录。密码我建议不要明文存哪怕课设也要养成习惯用 MD5 加密后再入库。别嫌麻烦答辩时这是一个加分的细节。商品表是核心表字段我设计了商品 ID、条码、名称、分类、单位、售价、库存。超市场景里条码是商品的唯一识别码所以我把条码设成了唯一索引收银时直接根据条码查商品效率高也不会重复。售价要注意用 DECIMAL 类型而不用 float 或 double float 做小数运算会有精度丢失金额算错在收银系统里是不可接受的。销售记录主表和明细表是典型的“头 行”结构。主表保存一次收银的总体信息订单号、收银员 ID、总金额、支付方式、创建时间。明细表保存这次收银里的每一件商品订单号、商品 ID、条码、商品名称、单价、数量、小计金额。为什么要把冗余的商品名称和条码也存进明细表因为商品价格和名称以后可能会改但历史订单必须保持那张小票当时的样子这叫快照是很多新手容易忽略的点。四张表的关系非常清晰用户一对多销售记录主表销售记录主表一对多销售记录明细表商品表和明细表则是通过商品 ID 关联。外键在 MySQL 里我建了但实际代码里没有太依赖数据库外键的级联操作因为 JDBC 控制事务更灵活后续我也会提醒你在代码里保证数据的完整性而不是全靠数据库的约束。2. 核心细节解析与实操要点2.1 登录模块会话管理到底在管理什么登录模块看似简单实际是很多同学的第一个坎。你要明白HTTP 协议是无状态的服务器处理完一个请求就把它忘了那它怎么知道一个请求是不是登录过的用户发来的答案是 Session。我实现登录的思路是这样的用户提交用户名和密码Servlet 先处理字符串比如忽略首尾空格再用 PreparedStatement 去数据库查查不到就返回“用户名或密码错误”查到了就把用户对象塞进 Session然后重定向到收银台页面。之后用户的每一次请求都会经过一个过滤器过滤器从 Session 里拿用户对象拿不到就直接踢回登录页。在过滤器判断用户登录状态时要注意一个坑如果你在 JSP 里直接用 if(session.getAttribute(user) null) 来判断那静态资源比如 CSS、JS、图片也会被拦下来因为静态资源请求也经过了过滤器。所以过滤器里要加一个排除判断把 /css、/js、/images、/login 这几个路径放行不然页面的样式全都会消失变成一个纯文字页面看起来特别像课设翻车现场。密码加密的具体做法我建议用 JDK 自带的 MessageDigest 对 MD5 进行哈希虽然 MD5 已经不算安全但课设够用。你也可以顺手加一个固定盐值比如在用户注册时把用户名拼到密码后面再做加密这样答辩时还能多说一个“防彩虹表”的点显得你有安全意识。2.2 收银台购物车用 Session 还是用数据库临时表收银台的核心交互是收银员不停地扫码商品一件一件加入当前这笔交易。问题来了这些“等待结算的商品”应该存在哪里我最早想的是数据库建一张临时购物车表后来觉得完全没必要因为这笔交易还没完成数据没落库临时表除了增加数据库压力没有任何好处而且还需要一套清空和清理机制课设阶段纯属给自己找事。正解是用 Session 里的一个 Map 来充当购物车。key 是商品条码 value 可以是一个自定义的 CartItem 对象包含商品信息和数量。前端每次扫到一个条码后端就从数据库查出商品然后再判断 Session 里面这个条码的 Map 里有没有有就把数量加一没有就新建一个条目放进去。这样整个收银过程的数据全部在内存中速度快代码直观而且天然跟着会话走用户登录以后数据就在退出登录就没了也不用清理。这里有一个细节要注意同样的条码你加进购物车时要查一次数据库结账时还要再查一次数据库两次查询之间如果商品价格被管理员改了就会导致页面显示的价格和最后落库的价格不一致。我当时的做法是把商品单价在加入购物车时复制一份存进 CartItem结账时直接用这一份价格来算总价避免中途改价导致顾客有意见。这是从真实商超系统里学来的逻辑答辩时主动提这个点老师会觉得你考虑问题很周全。2.3 收银结算事务为什么要手动控制收银结算这个动作是项目里最容易出 bug 的地方也是面试官和答辩老师最爱问的地方。一次结算我要做的事情有生成销售记录主表、批量生成明细表、更新商品的库存、清空 Session 里的购物车。这四个步骤只要中间任何一步失败了数据库就可能出现“订单建了但库存没扣”或者“库存扣了但订单没建出来”的脏数据。要解决这个问题就得引入事务。事务的本质就是“要么全做要么全不做”。JDBC 里默认是每条 SQL 自动提交所以你必须手动关闭自动提交在业务层的 Service 方法里开启事务所有操作做完之后统一提交如果中途抛了异常就回滚到事务开始之前的状态。我在课设代码里的做法是在 OrderService 的 checkout 方法里获取数据库连接调用 conn.setAutoCommit(false)然后依次完成插入主表、插入明细表、扣减库存三个操作全部成功后再 conn.commit()任何一个环节抛异常就 conn.rollback()最后在 finally 里把连接状态复位并关闭。这里有一个很容易踩的坑千万不要在 DAO 层里自己 new 一个连接否则 Service 层开启的事务管不到 DAO 层那条连接事务就形同虚设。正确做法是把连接对象从 Service 层传下去或者用 ThreadLocal 绑定到当前线程。2.4 扣库存与防超卖一条 UPDATE 语句搞定的事情扣库存为什么容易超卖因为你在 Java 代码里先 SELECT 查询库存判断库存大于零再执行 UPDATE 更新这个过程在并发情况下很可能两个人同时查到库存剩 1 件同时判断可以买然后都成功扣减库存就变成负数了。课设阶段大家通常不会真的遇到并发场景但答辩老师很可能会问“如果两个收银员同时卖最后一瓶水怎么办”。我的解决办法很简单把判断和扣减合并成一条原子性的 UPDATEUPDATE product SET stock stock - 1 WHERE id ? AND stock 1;这条 SQL 是数据库层面的原子操作只有当库存大于等于购买数量时才会更新成功受影响的行数就是 1否则是 0。在代码里我拿受影响行数做判断如果是 0就说明库存不够直接回滚事务并提示“商品库存不足”。这个方案又简单又可靠比用 synchronized 锁住代码块靠谱得多也更容易讲明白。还有一个细节是扣库存前如果你用乐观锁就要在商品表加一个 version 字段UPDATE 的时候把 version 也带上数据更新成功就把 version 加一这样就算两个人同时读到同一版本后提交的人也会失败重试。课设用上面那条 stock ? 的 SQL 就够了乐观锁了解即可可以在报告里提一嘴作为扩展点。3. 实操过程与核心环节实现3.1 项目目录结构与运行环境我的项目用的是标准 Maven Web 项目结构虽然没有引入多少第三方依赖但用 Maven 管理 JAR 包比手动去网上下载 jar 塞进 WEB-INF/lib 要省心太多老师检查项目时看到 pom.xml 也会觉得你比较规范。目录结构大致如下supermarket-cashier ├── pom.xml ├── src │ ├── main │ │ ├── java │ │ │ └── com/shop │ │ │ ├── entity // User, Product, SaleOrder, SaleItem, CartItem │ │ │ ├── dao // UserDao, ProductDao, SaleDao │ │ │ ├── service // UserService, ProductService, OrderService │ │ │ ├── servlet // LoginServlet, LogoutServlet, ProductManageServlet, CashierServlet, CheckoutServlet │ │ │ ├── filter // AuthFilter │ │ │ └── util // DBUtil, MD5Util │ │ ├── resources │ │ │ └── db.properties │ │ └── webapp │ │ ├── css │ │ ├── js │ │ ├── WEB-INF │ │ │ └── jsp // login.jsp, cashier.jsp, product.jsp, saleList.jsp │ │ └── index.jsp └── sql └── init.sql运行环境我建议 JDK 8 或 JDK 11、Tomcat 8.5/9、MySQL 5.7 或 MySQL 8.0。这几个版本都是 JavaWeb 课设里最常用的组合网上教程也多出了问题容易搜到答案。如果你电脑上装的是 Tomcat 10要特别小心因为 Tomcat 10 默认把包名从 javax.servlet 换成了 jakarta.servlet很多老教程的代码直接粘过去会报找不到包我当时就因为这个折腾了很久后面单独会说。3.2 数据库初始化脚本数据库我命名为 supermarket_db初始化脚本从建库到插入测试数据一条龙。以下是简化后的建表脚本方便你直接跑CREATE DATABASE IF NOT EXISTS supermarket_db DEFAULT CHARACTER SET utf8mb4; USE supermarket_db; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT CASHIER, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(32) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, category VARCHAR(50), unit VARCHAR(10) DEFAULT 瓶, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 ); CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, pay_type VARCHAR(20) DEFAULT CASH, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sale_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, barcode VARCHAR(32) NOT NULL, product_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL ); INSERT INTO user (username, password, role) VALUES (admin, MD5(admin123), ADMIN), (cashier01, MD5(123456), CASHIER); INSERT INTO product (barcode, name, category, unit, price, stock) VALUES (6901234567890, 农夫山泉矿泉水, 饮料, 瓶, 2.00, 100), (6901234567891, 可口可乐, 饮料, 瓶, 3.50, 80), (6901234567892, 康师傅红烧牛肉面, 方便食品, 桶, 4.50, 50);这里插入用户密码的时候直接调用了 MySQL 的 MD5 函数所以我在代码里加密用户密码时也用 MD5 做了一次哈希两边保持一致登录时拿输入密码哈希后的值去比对就能匹配上。3.3 数据库连接工具与登录核心代码数据库连接我用的是 Druid 连接池虽然课设没要求但提前接触连接池没有坏处。db.properties 里配置驱动、地址、用户名、密码和初始连接数。这里有一个高频坑MySQL 8.0 的驱动类名是 com.mysql.cj.jdbc.Driver不是以前老版的 com.mysql.jdbc.DriverURL 后面还需要加上 useSSLfalse 和 serverTimezoneAsia/Shanghai不然连不上或者报时区错误。DBUtil 的部分代码如下package com.shop.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.sql.Connection; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static DataSource dataSource; static { try (InputStream is DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(is); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败: e.getMessage()); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }登录的 LoginServlet 并不复杂但是我建议把业务判断放到 Service 层不要让 Servlet 直接写 SQL。一个简化版的登录逻辑如下package com.shop.servlet; import com.shop.entity.User; import com.shop.service.UserService; import com.shop.util.MD5Util; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; WebServlet(/login) public class LoginServlet extends HttpServlet { private final UserService userService new UserService(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); String username req.getParameter(username); String password MD5Util.md5(req.getParameter(password)); User user userService.login(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(loginUser, user); resp.sendRedirect(cashier); } else { req.setAttribute(error, 用户名或密码错误); req.getRequestDispatcher(/WEB-INF/jsp/login.jsp).forward(req, resp); } } }注意登录失败我用的不是重定向而是请求转发这样能把 error 属性带到登录页显示URL 也不会变成 login.jsp体验上更好一些。3.4 核心代码收银结算业务中最不能出错的环节结算是最难写的一部分因为要同时操作三张表中间还不能断开。我把这段的完整思路拆成三步第一步从前端拿到支付方式和购物车快照第二步生成主表订单第三步逐条生成明细并扣库存。下面是简化之后的 OrderService 核心方法package com.shop.service; import com.shop.dao.SaleDao; import com.shop.entity.CartItem; import com.shop.entity.SaleOrder; import com.shop.entity.SaleOrderItem; import com.shop.util.DBUtil; import java.math.BigDecimal; import java.sql.Connection; import java.sql.SQLException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; public class OrderService { public String checkout(int userId, String payType, MapString, CartItem cart) throws SQLException { if (cart null || cart.isEmpty()) { throw new SQLException(购物车为空无法结算); } Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); SaleOrder order new SaleOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setPayType(payType); BigDecimal total BigDecimal.ZERO; for (CartItem item : cart.values()) { total total.add(item.getSubtotal()); } order.setTotalAmount(total); SaleDao saleDao new SaleDao(); int orderId saleDao.insertOrder(conn, order); for (CartItem item : cart.values()) { SaleOrderItem detail new SaleOrderItem(); detail.setOrderId(orderId); detail.setProductId(item.getProduct().getId()); detail.setBarcode(item.getBarcode()); detail.setProductName(item.getName()); detail.setPrice(item.getPrice()); detail.setQuantity(item.getQuantity()); detail.setSubtotal(item.getSubtotal()); saleDao.insertOrderItem(conn, detail); boolean ok saleDao.deductStock(conn, item.getProduct().getId(), item.getQuantity()); if (!ok) { throw new SQLException(商品库存不足: item.getName()); } } conn.commit(); return order.getOrderNo(); } catch (SQLException e) { if (conn ! null) { conn.rollback(); } throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } } private String generateOrderNo() { String time new SimpleDateFormat(yyyyMMddHHmmss).format(new Date()); String uuid UUID.randomUUID().toString().replace(-, ).substring(0, 6).toUpperCase(); return time uuid; } }这段代码有一个细节值得说生成订单号我用了时间戳加 6 位随机 UUID虽然课设压力下有概率重复但概率极低。为了保险可以在 sale_order 表给 order_no 唯一索引万一重复了直接抛异常也能触发事务回滚。用数据库自增 ID 当订单号不是不行但真实业务里订单号通常要求可读性强自己生成更接近生产环境做法。扣库存的 SQL 在上面已经说过就是那一行受影响的原子 UPDATE。真正跑通之后你会觉得事务这东西也没那么玄乎无非是把一组操作绑在一起出错就回滚关键是你要有这个意识并且知道它在代码里怎么落。3.5 部署与运行步骤项目写完之后部署运行也是一门课。我先说我的运行流程你再自己调整。第一步用 IDEA 打开 Maven 项目等待依赖下载完成第二步在 MySQL 里执行 init.sql 脚本第三步修改 db.properties 里的用户名和密码第四步配置 TomcatArtifact 选 war explodedApplication context 随便填一个比如 /supermarket第五步启动 Tomcat访问 http://localhost:8080/supermarket看到登录页就说明环境通了。有一个 IDEA 特有的坑我提醒一下如果你的项目编码不是 UTF-8启动后页面中文显示乱码大概率是 IDE 默认编码问题。我一般会在 Settings 里把 File Encodings 的 Global Encoding、Project Encoding 和 Properties Files 全部改成 UTF-8同时给 Tomcat 的 VM options 加上 -Dfile.encodingUTF-8这样中文基本就不会乱了。如果你是第一次跑连数据库的 Web 项目很可能会遇到“找不到驱动类”的问题。用 Maven 的话检查 pom.xml 里有没有 mysql-connector-java 的依赖不用 Maven 的话确保 mysql 驱动 jar 已经被放到 Tomcat 或项目的 WEB-INF/lib 下并且没有被 IDE 忽略。这种问题不是代码逻辑错而是包没进来看到 ClassNotFoundException 先往这个方向排查。4. 常见问题与排查技巧实录4.1 典型报错速查表我把这个项目里最容易出现的几类问题整理成了一张表你在跑代码的时候遇到直接对照就行。问题现象可能原因解决办法java.lang.ClassNotFoundException: com.mysql.jdbc.DriverMySQL 驱动版本与驱动类名不匹配使用 mysql-connector-java 8.x驱动类改成 com.mysql.cj.jdbc.Driverjava.sql.SQLException: The server time zone value ...MySQL 8 时区配置问题JDBC URL 加 serverTimezoneAsia/Shanghaijava.sql.SQLException: Access denied for user数据库用户名或密码错误检查 db.properties 配置确认账号有访问权限页面中文全部乱码编码不一致或 Tomcat 未设置IDEA 全局编码改 UTF-8JSP 加 pageEncodingUTF-8过滤器里设置 request 编码Tomcat 8080 端口被占用之前启动过多个 Tomcat 实例关闭占用进程或改 Tomcat 端口号HTTP Status 404Servlet 映射路径写错或项目没部署成功检查 WebServlet 里的路径确认浏览器访问的是项目上下文加路径页面能打开但没有样式静态资源被过滤拦截在 AuthFilter 中放行 /css、/js、/images 路径库存变成负数并发下判断与更新不是原子操作改成 UPDATE product SET stock stock - ? WHERE id ? AND stock ?4.2 并发扣库存为什么还是超卖我前面讲防超卖用了一条原子 UPDATE但如果你只是把查询和更新分开写还是会出现超卖。很多同学照着网上的代码抄抄的时候没理解为什么能防超卖就很容易改着改着把关键条件弄丢。比如有人会先写一句 SELECT stock FROM product WHERE id ?然后在 Java 里判断这个 stock 是否大于 0最后再执行 UPDATE。这个判断在单机、单用户、顺序操作时完全没问题但只要两个请求同一时刻进来两边都读到了 stock1两边都判断能卖两边都 UPDATE 成 0数据库里没有负数但实际卖出了两件货账面库存就错了。解决办法就是别在 Java 里做“读后判断再写”而是直接让数据库帮你完成判断和更新。我推荐的 SQL 已经把更新条件和判断条件合在一起了这条语句执行后如果影响了 0 行说明库存不足这时候再插入订单就不应该继续。配合事务的回滚整个结算要么成功要么失败库存不会多扣也不会少扣。还有一个小建议结算时不要在循环里反复调用 getConnection 获取新连接一个事务里必须用同一个 Connection否则你的事务控制覆盖不到所有 SQL 操作。这是初学者最容易犯的错之一我在最初版本里就踩过最后调试了很久才定位到是连接没有复用。4.3 答辩时的高频问题与应对思路课设答辩老师的问题通常不会太难但很喜欢在关键细节上追问。我把自己被问过的和同学被问过的问题整理一下你可以提前准备。第一个问题是“你这个项目是三层架构吗每一层的作用是什么”。你如果用了 Service 层和 DAO 层就可以回答Servlet 负责接收请求和跳转页面Service 层负责业务规则比如事务管理和库存判断DAO 层负责单纯的数据库读写。这个回答一出来老师就知道你真的做过分层。第二个问题是“购物车为什么放在 Session 里而不是数据库”。你可以回答购物车是临时数据生命周期只存在于一次收银过程中Session 天然支持会话隔离用数据库存反而需要额外的清理机制成本高收益低。顺带还可以提一句 Session 的并发限制说明如果以后做分布式商城会把购物车迁移到 Redis这是一个扩展思路显得你有大局观。第三个问题是“如果商品删除或者价格修改历史订单怎么办”。这个问题可以直接用我在明细表里保存商品名称和价格的快照来解释每次结算时把这些信息复制到明细表之后商品表怎么改都不影响历史订单报表统计依然准确。这个问题能接住答辩基本就稳了。第四个问题是“登录密码怎么存储的”。你要主动说用了 MD5 加固定盐值加密还可以补一句真实项目会使用 BCrypt课设选择 MD5 是为了降低理解成本。这样既展示你知道安全常识又解释了课设技术选型的合理性不会被质疑为什么不用更新的加密算法。4.4 Tomcat 10 的兼容性问题单独提醒现在很多学校机房或者同学自己电脑上的 Tomcat 版本已经变成 10 了但这门课的老教程基本都是 Tomcat 8 或 9 的写法。Tomcat 10 最大的变化是 Servlet 包名从 javax.servlet 换成了 jakarta.servlet。这意味着你所有 import javax.servlet.* 的代码在 Tomcat 10 里都会直接编译报错哪怕代码逻辑完全正确。我当时一开始没注意启动 Tomcat 后就看到一长串 ClassNotFound / NoClassDefFoundError 报错排查了快两个小时才发现问题。解决办法有两个要么把 Tomcat 降级到 9.0 系列要么把所有 import javax.servlet 的代码批量替换成 import jakarta.servlet。我建议你优先用 Tomcat 9因为网上搜资料时大多数答案都是基于 javax 的省得一边改代码一边还要适应新版差异。如果你的课设报告要求说明运行环境建议直接写清楚你实际用的版本不要照抄网上的配置文档否则老师一问版本就露馅。5. 从课设到后续扩展的一点建议超市收银系统做完之后如果你还想在报告里多写点内容或者后面实习面试时拿得出手可以考虑把项目往几个方向扩展一下。第一个是增加图表统计页把每天的销售额、每个品类的销量用 ECharts 画成折线图和柱状图视觉效果直接提升一个档次技术上也只多了一个 JS 库和几条统计 SQL。第二个是把结算逻辑改成支持折扣和会员价这样可以展示你对优惠分摊和金额计算的思考。第三个是把在线支付模拟变成一个小功能虽然课设不能真的接支付平台但用一张表模拟支付回调也能写成报告的创新点。我个人在实际操作中的体会是课设最不值钱的是代码本身最值钱的是你能不能把每一个设计决策背后的理由说清楚。超市收银系统这个题目好就好在它的每个模块都不需要太多代码量但每个模块都能问出“为什么这么做”的问题为什么用 Session 存购物车为什么扣库存要用一条 SQL为什么明细表要存商品快照。你把这些问题都回答清楚了报告写得扎实代码也跑得通这门课设基本上就是高分预定。最后分享一个小技巧提交源代码之前一定要在别人电脑上或者换一个浏览器跑一遍完整流程尤其是登录、加购、结算、查看销售记录这条主链路。很多人自己开发机上跑得挺好结果因为数据库配置写死了本机密码或者 Tomcat 路径带了个人用户名交到老师那里一跑就挂。哪怕代码写得再漂亮演示时候连不上数据库都是大扣分项。所以打包前把 db.properties 里面的连接信息改成通用的 localhost用户名密码写清楚再写一个 README 说明运行步骤这种小细节真的很加分。