ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

超市管理系统数据库课程设计:从建表到答辩的完整实战指南

超市管理系统数据库课程设计:从建表到答辩的完整实战指南

简介:一款基于Java Web的超市管理系统,是数据库课程设计的完整高分项目,面向计算机相关专业(人工智能、通信工程、自动化、电子信息、物联网等)的在校学生、教师及企业员工,尤其适合需要完成课程设计、毕业设计或项目初期阶段演示的开发者。包内共125个文件,以30个Java源文件、20个JSP页面、23个JavaScript脚本、18个PNG图片、10个XML配置文件及5个CSS样式为主,涵盖前端展示、后端逻辑、数据库配置与界面素材,压缩包仅3.05MB,目录紧凑便于快速定位。目前已有40人学习下载。项目源码已通过运行测试,并获导师指导认可,答辩评审分达95分,可直接用于课程设计提交或毕业设计基础;代码结构清晰,基础较好的学习者还可在此基础上扩展新功能,深入理解Java Web开发与数据库设计流程。对于初学者,丰富的文件类型与完整文档也是一条直观的进阶路径。

1. 为什么“超市管理系统”是数据库课程设计里最值得抄的作业

期末周凌晨两点,网盘里拖下来的那个“Java Web 超市管理系统 数据库课程设计 全部资料+详细文档+高分项目.zip”已经解压了四十分钟,你盯着满屏的.java文件和Word文档,不知道先点哪一个。这是绝大多数人第一次拿到这类web项目的真实状态——资料越全,越不知道从哪下手。这个标题背后其实是一套非常标准的教学型项目:用Java Web技术栈做一套能跑通的超市进销存系统,数据库设计是考核重点,附带课程设计报告和答辩材料。它价值不在代码多花哨,而在它把所有数据库课的必考知识点——建表约束、外键关系、事务处理、触发器、统计查询——全部装进了一个看得见摸得着的业务场景里。适合两类人:正在做数据库课设、需要快速上手并应付答辩的学生;以及想找一个完整Java Web项目练手、把SSM或Spring Boot全家桶串起来的初学者。这文章就按“这是什么、怎么搭、哪些坑”往下拆。

2. 先定技术栈再动代码:Servlet时代还是 Spring Boot + MyBatis 时代

2.1 一套 zip 资料里是哪种 Java Web 方案,三分钟看出来

这类课设资料网上流传的两个时代:老一代是 JSP + Servlet + JDBC + MySQL,新一代是 Spring Boot + MyBatis + Thymeleaf/JSP + MySQL。 两者都叫“Java Web”,但打开方式完全不一样。老方案里你能找到 WEB-INF 目录、web.xml、一堆 .jsp 页面,部署要靠 Tomcat 手动添加 Artifact;新方案里你会看到 pom.xml、src/main/java、application.properties,一个 main 方法就能启动内置 Tomcat。你手里这套是哪一种,决定了后面所有操作。

分辨方法很笨但很有效:先在根目录找 pom.xml 或 build.gradle,有就是 Maven/Gradle 工程,大概率是 Spring Boot 或 SSM;只有 .classpath 和 .project,就是老式 Eclipse 动态 Web 工程。再看是否有 web.xml,Spring Boot 默认没有,JSP 老工程必有。最后看配置文件的扩展名——.properties 或 .yml 是 Spring Boot,context.xml 或 jdbc.properties 且代码里用 Class.forName 加载驱动是老式 JDBC。

识别点老式 JSP+Servlet+JDBCSpring Boot + MyBatis
工程入口需要外置 Tomcat 启动main 方法直接启动
配置文件web.xml / context.xmlapplication.properties / application.yml
数据访问Statement / PreparedStatementMapper 接口 + XML / 注解 SQL
页面技术JSP 直出Thymeleaf 或 JSP 均可
答辩难度底层好讲,代码量大工程化强,容易被深问框架原理

老式方案的优点是每一条数据流都暴露在眼前,答辩时老师问“这个查询是怎么执行的”,你能从 JSP 一路讲到 JDBC 驱动;缺点是增删改查要写大量样板代码,改一个字段要动 Java、JSP、SQL 三处。Spring Boot + MyBatis 的好处是跟现在企业级web开发的习惯接轨,拿这套东西去准备 java 面试题也不算跑偏;坏处是新手答辩被追问“Spring 容器是什么、MyBatis 代理原理”容易卡壳。如果包里两种都有,课设目的优先选老方案,面试导向优先选新方案。我一般建议按你自己 Java 基础来,哪个能完整讲透选哪个,别贪框架新。

2.2 超市系统的模块边界:最少六张表加三类角色的业务闭环

不管选哪套技术栈,业务模型是一样的。超市管理系统本质是进销存:先有供应商供货,商品入库形成库存,收银台销售扣减库存,期间要有人维护商品和用户权限。按这个流程划,最少六张业务表加上若干辅助表就能形成闭环:用户表管理登录和角色,商品分类表做层级归档,商品表存核心信息,供应商表管货源,进货单和进货明细记录入库,销售单和销售明细记录出库,外加一张库存流水表做审计。

角色权限通常是课设里容易被忽略却很好得分的一块。至少要有管理员和收银员两个角色:管理员负责商品维护、进货入库、查看统计报表;收银员只能开销售单和查库存。别小看这个设计,答辩时老师大概率会问“怎么控制收银员不能删商品”,你只要答出“请求先过过滤器校验 session 里的角色,再进业务层”就过关了。不用做细粒度权限模型,那超出课设范围了。

资料包里的文档通常会把这些模块画成用例图,但代码里未必全部实现。拿到手以后先对着文档把已实现的功能列个清单,哪些页面能点、哪些按钮是死链,心里有数。最实在的做法是顺着一个完整业务流走通:管理员登录 → 新增商品 → 新增进货单 → 库存增加 → 收银员登录 → 开销售单 → 库存减少 → 查统计报表。这一圈走通了,系统性功能就都覆盖了。

2.3 本地跑通环境:JDK、Tomcat 和 MySQL 8 的一次性配置

环境是第一个劝退点。老式 JSP+Servlet 项目建议 JDK 8 或 11 配 Tomcat 9,别用 Tomcat 10,因为 Tomcat 10 把包名从 javax.servlet 改成了 jakarta.servlet,老项目源码直接编译不过。Spring Boot 项目只要 JDK 8 以上和 Maven 就能跑,内置 Tomcat 省掉一步。MySQL 这边,如果 SQL 脚本是几年前写的,强烈建议装 MySQL 8.0 而不是 5.7,除非脚本里明确用了 5.7 的语法——反过来,脚本如果是老版本写的,导入 MySQL 8 可能吃 sql_mode 的亏,这在后面避坑章节会细说。

配置文件的常见写法如下,Spring Boot 项目直接改 application.properties:

spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=123456 server.port=8080 server.servlet.encoding.force=true

这段配置里 driver-class-name 必须用 com.mysql.cj.jdbc.Driver 而不是旧版的 com.mysql.jdbc.Driver,这是 MySQL 8 的硬性要求,用错直接报 ClassNotFoundException。URL 上的三个参数一个都不能省:serverTimezone 不设置会报时区异常,characterEncoding 不设置中文写入数据库变问号,useSSL 不设 false 会在启动时打印长长一条 SSL 警告。端口默认 8080,如果本地被占,改成 8081 或其他空闲端口即可。

老式 JDBC 项目改这些参数的位置不同,通常在 src/jdbc.properties 或 src/util/DBUtil.java 里写死。把连接串改成上面同样格式,注意密码和你本地 MySQL 的实际密码一致。资料包里默认密码如果是 123456 而你本机密码不是,数据库连不上时第一个查这里。最后在命令行验证一下能连上库再启动项目,能省不少排查时间:

mysql -uroot -p --default-character-set=utf8mb4 supermarket -e "SELECT 1"

这行命令的意思是:用 root 用户连接本地 MySQL,指定字符集 utf8mb4,进入 supermarket 库,执行 SELECT 1 测连通性。输出 1 就说明库和用户都没问题,后面项目连不上就要回头查配置文件而不是数据库本身了。

3. 数据库设计决定分数上限:核心建表、触发器与索引

3.1 商品、进货、销售三张主表的字段级设计

超市系统的数据库设计是课设的命根子,文档里最厚的部分一定是 ER 图和数据字典。表之间关系一句话讲清:分类表 1 对多商品,供应商 1 对多商品,商品 1 对多进货明细和销售明细,进货单和销售单作为主表挂明细子表。这个模型规整,画 ER 图好看,老师一眼能看懂,分数下限就保住了。字段设计有几个容易出彩的地方,先说结论:金额一律 DECIMAL;时间用 DATETIME;数量和单价不允许负数;价格在明细表里冗余存储。

CREATE TABLE t_category ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(50) NOT NULL UNIQUE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_goods ( id INT AUTO_INCREMENT PRIMARY KEY, bar_code VARCHAR(32) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, category_id INT NOT NULL, supplier_id INT NOT NULL, spec VARCHAR(50) DEFAULT '', unit VARCHAR(10) DEFAULT '件', stock_qty INT NOT NULL DEFAULT 0, sale_price DECIMAL(10,2) NOT NULL, purchase_price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_goods_category FOREIGN KEY (category_id) REFERENCES t_category(id), CONSTRAINT fk_goods_supplier FOREIGN KEY (supplier_id) REFERENCES t_supplier(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_sale ( id INT AUTO_INCREMENT PRIMARY KEY, ticket_no VARCHAR(32) NOT NULL UNIQUE, total_amount DECIMAL(10,2) NOT NULL, receive_cash DECIMAL(10,2) NOT NULL DEFAULT 0, change_amount DECIMAL(10,2) NOT NULL DEFAULT 0, operator_id INT NOT NULL, sale_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE t_sale_item ( id INT AUTO_INCREMENT PRIMARY KEY, sale_id INT NOT NULL, goods_id INT NOT NULL, qty INT NOT NULL, sale_price DECIMAL(10,2) NOT NULL, subtotal DECIMAL(10,2) NOT NULL, CONSTRAINT fk_item_sale FOREIGN KEY (sale_id) REFERENCES t_sale(id), CONSTRAINT fk_item_goods FOREIGN KEY (goods_id) REFERENCES t_goods(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个字段设计的细节说明。bar_code 设唯一索引,这是超市系统的自然键,答辩时老师问“为什么不只用自增 id 定位商品”,答“扫码枪录入商品时按条码查,唯一索引保证不会一码两货”就够用了。t_sale_item 里的 sale_price 和 subtotal 是故意冗余的——商品价格日后会改,但已经卖出去的订单必须保留成交时的价格,这就是快照思想。很多人课设做成 VIEW 实时关联商品表价格,改一次价,历史订单金额全变,这是大忌。stock_qty 在 t_goods 里属于冗余字段,可从明细推算,但每次查库存都去 SUM 明细会把数据库压垮,所以保留冗余并用事务维护一致性。这笔账答辩时主动讲出来是明显加分项。

3.2 触发器管库存:一条 INSERT 的连锁动作

库存扣减是超市系统里最容易写成脏数据的地方。常见做法有两种:业务代码里先 UPDATE 商品库存再 INSERT 销售明细;或者用数据库触发器在明细写入时自动扣减。课程设计我建议用触发器,原因有两个:答辨时“数据库高级特性”这一栏直接有得写;代码里少写一行 UPDATE,事务边界的复杂度会降很多。副作用是库存和明细的强一致由数据库保证,不容易因为漏写代码产生幽灵库存。

DROP TRIGGER IF EXISTS trg_sale_item_stock; DELIMITER // CREATE TRIGGER trg_sale_item_stock AFTER INSERT ON t_sale_item FOR EACH ROW BEGIN UPDATE t_goods SET stock_qty = stock_qty - NEW.qty WHERE id = NEW.goods_id; END // DELIMITER ;

这个触发器的执行逻辑是:每当 t_sale_item 表插入一行销售明细,数据库自动执行一次商品库存扣减,扣减数量等于刚插入那行的 qty 字段,NEW 关键字代表新插入行的引用。注意必须在 AFERT INSERT 做,不能在 BEFORE,因为库存扣减依赖明细行已经持久化。一个常见的翻车点是有人写 FOR EACH ROW BEGIN UPDATE t_sale_item SET ... END,试图在同一张表上做操作,MySQL 不允许触发器修改自己正在操作的表,会直接报错——正确姿势是只更新其他表。另外库存不足的检查建议留在业务层先查一遍,触发器里也可以用 SIGNAL SQLSTATE '45000' 抛异常阻止插入,但对新手来说,先把自动扣减跑通,再做校验。

3.3 答辩加分项:多表视图和一条统计 SQL

视图和统计查询是拉开分数差距的地方。文档里画了 ER 图、写了表结构只能算完成任务,能拿出“库存预警视图”和“按日销售统计”才叫亮点。库存预警视图要解决的问题是:哪些商品快卖完了需要补货。它不新建表,只是把商品表里库存量低于阈值的行捞出来,再关联分类和供应商名,展示出可读的信息。

CREATE OR REPLACE VIEW v_low_stock AS SELECT g.id, g.name, g.bar_code, g.stock_qty, g.sale_price, c.name AS category_name, s.name AS supplier_name FROM t_goods g JOIN t_category c ON g.category_id = c.id JOIN t_supplier s ON g.supplier_id = s.id WHERE g.stock_qty <= 10 AND g.status = 1; SELECT DATE(sale_time) AS sale_date, COUNT(DISTINCT ticket_no) AS order_count, SUM(total_amount) AS total_sales FROM t_sale GROUP BY DATE(sale_time) ORDER BY sale_date DESC;

第一条是视图定义,执行后 v_low_stock 就相当于一张只读表,页面里直接 SELECT * FROM v_low_stock 就能拿预警列表。阈值 10 是写死的,演示时跟老师说“这里可以做成参数配置,按商品类别设定不同阈值”。第二条统计 SQL 是按天汇总销售额,GROUP BY DATE() 把 DATETIME 截断到天级,COUNT(DISTINCT ticket_no) 统计订单数而不是明细行数——这个细节能体现你分得清单头和明细,打印小票和统计订单量都以 t_sale 为准。如果导出的 SQL 脚本在 MySQL 8 里跑出来 GROUP BY 报错,多半是 sql_mode 开了 ONLY_FULL_GROUP_BY,这里先留个印象,避坑章再展开。

4. 把数据库增删改查落成页面:Controller、Service、DAO 怎么写

4.1 三层架构的包名规范和一次请求的完整路径

拿到资料后别急着看代码,先看包结构。一个标准的 Spring Boot + MyBatis 课设项目,包名通常是 com.xxx.supermarket 下面拆四个包:controller 放页面跳转和接口,service 放业务逻辑,mapper 或 dao 放数据访问,entity 或 pojo 放实体类。老式 JSP+Servlet 项目则对应 servlet 包、service 包和 dao 包。这个分层不是摆设,答辩时老师最喜欢指着一个方法问“你从页面点这个按钮到数据库执行这条SQL,中间经过哪几层”,你能把路径讲完整,Java Web 这关就过了。

一次新增商品的完整路径是:浏览器提交表单 → Tomcat 把请求交给 DispatcherServlet → 映射到 GoodsController 的 addGoods 方法 → Controller 取出请求参数封装成 Goods 对象 → 调用 GoodsService.insert → Service 里可能做参数校验或重复条码检查 → 调用 GoodsMapper.insert → MyBatis 执行 INSERT SQL → 返回自增主键 → 逐层返回,页面刷新列表。每层只干一件事:Controller 不写 SQL,Mapper 不写业务判断,Service 不直接接触 HttpServletRequest。不按这个规范写的代码往往几百行堆在一个 Servlet 里,跑是能跑,答辩一被追问就露怯。

@Mapper public interface GoodsMapper { @Insert("INSERT INTO t_goods(bar_code, name, category_id, supplier_id, spec, unit, stock_qty, sale_price, purchase_price, status) " + "VALUES(#{barCode}, #{name}, #{categoryId}, #{supplierId}, #{spec}, #{unit}, #{stockQty}, #{salePrice}, #{purchasePrice}, #{status})") @Options(useGeneratedKeys = true, keyProperty = "id") int insert(Goods goods); @Select("SELECT * FROM t_goods WHERE bar_code = #{barCode}") Goods findByBarCode(String barCode); @Select("SELECT * FROM t_goods WHERE name LIKE CONCAT('%', #{keyword}, '%') ORDER BY id DESC LIMIT #{offset}, #{pageSize}") List<Goods> search(@Param("keyword") String keyword, @Param("offset") int offset, @Param("pageSize") int pageSize); }

这段 Mapper 接口有几个值得注意的参数细节。@Options(useGeneratedKeys = true, keyProperty = "id") 表示插入后把数据库自增的 id 回填到传入对象的 id 字段里,后续要用这个 id 写流水时不用重新查一遍。#{name} 是 MyBatis 的预编译占位符,会生成 ? 形式的 PreparedStatement,既能防 SQL 注入,又能在日志里清楚看到参数值——绝不要用字符串拼接 SQL,这是新手最容易踩的坑。search 方法的 LIKE 查询用了 CONCAT('%', #{keyword}, '%'),比直接写 '%${keyword}%' 安全得多,后者是典型的注入漏洞写法,答辩老师看到了会直接扣分。

4.2 结账台事务:商品出库和流水写入必须同生共死

超市系统里唯一必须开事务的地方是结账台。一笔销售要同时做三件事:往 t_sale 插销售单主记录、往 t_sale_item 插若干条明细、扣减商品库存。这三件事任何一个失败,账就对不上。扣了库存没写流水,盘点时库存凭空少一件;写了流水没扣库存,钱收了库存还在。解决这个问题就靠数据库事务把这三步包在一个原子操作里,要么全部成功,要么全部回滚。业务层加一个 @Transactional 注解是最标准的做法。

@Service public class SaleService { private final SaleMapper saleMapper; private final SaleItemMapper saleItemMapper; public SaleService(SaleMapper saleMapper, SaleItemMapper saleItemMapper) { this.saleMapper = saleMapper; this.saleItemMapper = saleItemMapper; } @Transactional(rollbackFor = Exception.class) public Sale checkout(SaleDTO dto) { // 1. 生成单号,写入销售单主表 Sale sale = new Sale(); sale.setTicketNo(generateTicketNo()); sale.setTotalAmount(dto.getTotalAmount()); sale.setReceiveCash(dto.getReceiveCash()); sale.setChangeAmount(dto.getReceiveCash().subtract(dto.getTotalAmount())); sale.setOperatorId(dto.getOperatorId()); saleMapper.insert(sale); // 2. 遍历购物车项,逐条写入销售明细 for (SaleItemDTO item : dto.getItems()) { SaleItem saleItem = new SaleItem(); saleItem.setSaleId(sale.getId()); saleItem.setGoodsId(item.getGoodsId()); saleItem.setQty(item.getQty()); saleItem.setSalePrice(item.getSalePrice()); saleItem.setSubtotal(item.getSalePrice().multiply(BigDecimal.valueOf(item.getQty()))); saleItemMapper.insert(saleItem); } return sale; } }

rollbackFor = Exception.class 这个参数必须写,不写的话 Spring 默认只对 RuntimeException 回滚,而 SQL 执行失败的异常通常包装成了受检异常,会导致事务不回滚。金额计算用 BigDecimal 而不是 double,double 的浮点误差在涉及钱的地方是不可接受的,这一点要能主动讲出来。generateTicketNo 是自定义方法,常见实现是日期加随机数,比如 20260601 加 4 位序列,保证并发时单号不重复。库存扣减在上面的代码里没出现,因为第 3 章的触发器已经接手了——这就是把一致性外包给数据库的好处,业务代码少一层出错概率。演示时如果发现库存没动,先查触发器有没有建成功,别怀疑 Java 代码。

4.3 搜索加分页:LIMIT 和 PageHelper 两种思路选一个

列表页必做搜索加分页,不做这个的课设基本告别高分。两条路:手写 LIMIT 或者用 PageHelper 插件。手写 LIMIT 更适合课程设计,因为能讲清原理——前端传 pageNum 和 pageSize,后端算 offset,SQL 里 LIMIT #{offset}, #{pageSize},再查一条 COUNT 总数算总页数。用 PageHelper 只是调用一行 startPage,原理被封装进了黑匣子,答辩时讲不透反而减分。这里给一个手写分页的 Service 层片段,展示怎么把逻辑讲清楚。

public PageResult<Goods> queryGoods(String keyword, int pageNum, int pageSize) { int offset = (pageNum - 1) * pageSize; List<Goods> list = goodsMapper.search(keyword, offset, pageSize); Long total = goodsMapper.countSearch(keyword); int totalPages = (int) Math.ceil(total.doubleValue() / pageSize); return new PageResult<>(list, total, pageNum, pageSize, totalPages); }

逻辑说明:pageNum 从 1 开始,所以数据库的 offset 要减一再乘 pageSize,比如第 2 页每页 10 条,offset 就是 10。totalPages 用 Math.ceil 向上取整,保证最后一页数据即使不足 10 条也能显示出来。配合的 countSearch 方法要一直保持 SELECT COUNT(1) FROM t_goods WHERE name LIKE CONCAT('%', #{keyword}, '%'),注意搜索条件必须和列表查询完全一致,否则总页数算错。页面上的分页栏就是一组带 pageNum 参数的链接,点击后重新发起查询。这套逻辑讲完,数据库增删改查的“查”就算吃透了。

5. 课程设计避坑指南:环境、编码、部署、演示的五个翻车点

5.1 MySQL 8 驱动类改名导致项目起不来

现象:启动项目报 ClassNotFoundException: com.mysql.jdbc.Driver,或者报 Communications link failure 连接失败。

原因:MySQL 8 开始把 JDBC 驱动类从 com.mysql.jdbc.Driver 改名为 com.mysql.cj.jdbc.Driver,资料包里如果是老式项目,配置文件里还是旧类名,驱动 jar 也是 5.x 版本,两边对不上就起不来。你本机如果装的是 MySQL 8 但 jar 是 mysql-connector-java 5.1.x,也会因为协议差异产生奇怪的握手错误。

解决:换掉驱动 jar 为 mysql-connector-java 8.0.x 以上,配置文件里的 driver-class-name 改成 com.mysql.cj.jdbc.Driver,URL 后面补上 serverTimezone=Asia/Shanghai&useSSL=false。老式项目要是找不到 jar 在哪,去 WEB-INF/lib 下面把旧 jar 删掉换新的,别只改配置不换 jar,那是白忙活。

5.2 中文乱码:三处编码必须一致

现象:页面上商品名称显示成一串问号,或者数据库里存进去就是乱码,改了 JSP 的 pageEncoding 也没用。

原因:编码链路有三段,任何一段不一致就乱码。第一段是 JSP 页面本身的编码,第二段是 Tomcat 接收请求参数时的解码编码,第三段是 Java 写数据库时连接串指定的编码。很多人只改第一段,后面两段没动,写进去还是乱码。

解决:JSP 第一行确认 pageEncoding="UTF-8";Tomcat 的 conf/server.xml 里给 Connector 加 URIEncoding="UTF-8" 属性;JDBC 连接串里带 characterEncoding=utf8;建库语句用 DEFAULT CHARACTER SET utf8mb4。四件事全做了,中文进出就通顺了。千万别在代码里手动 new String(bytes, "ISO-8859-1") 这种上古解码操作,越转越乱。

5.3 IDEA 部署工件没配好,启动后永远 404

现象:Tomcat 启动日志显示成功,浏览器访问 localhost:8080/项目名 却 404,IDEA 控制台也没有报错。

原因:老式 JSP+Servlet 项目在 IDEA 里要靠 Artifact 部署,Run Configuration 里的 Deployment 标签下没添加 war exploded,或者 Application context 配成了根路径 / 而不是 /项目名。Tomcat 起来了但没加载你的应用,自然 404。

解决:编辑运行配置,Deployment 标签里点加号选 Artifact,把 war exploded 加进去,Application context 填 /supermarket。然后点开 Artifact 列表,确认 Output Layout 里有 WEB-INF 目录且 lib 下有 MySQL 驱动 jar。改完重新启动,访问地址别用 localhost:8080,用 localhost:8080/supermarket/。另外强烈建议用 Tomcat 9,它和 javax.servlet 老代码兼容,Tomcat 10 会因包名变更让老项目直接编不过。

5.4 导入的 SQL 脚本跑不通:触发器顺序、sql_mode 与修改表结构

现象:命令行执行 source 导入 SQL 文件,报错停在某一行,有时是 CREATE TRIGGER 报错,有时是 DELETE 或 UPDATE 操作受影响行数为 0 但也没报错。

原因:常见的三种。触发器或存储过程在脚本里没有用 DELIMITER 包裹定界符,MySQL 客户端把分号当成语句结束符,CREATE TRIGGER 的 BEGIN...END 块被拦腰截断;脚本是老 MySQL 5.7 写的,在 MySQL 8 里跑 SELECT 语句若没带上分组字段以外的列,被 ONLY_FULL_GROUP_BY 模式拦下;导入时如果先导明细表、后导主表,外键约束会让 INSERT 失败。

解决:导入前检查脚本里触发器部分是否写了 DELIMITER // 和结尾的 DELIMITER;连接 MySQL 时用 --default-character-set=utf8mb4 避免脚本里的中文注释变乱码;导入顺序先建库、再建表、最后导数据,严格按主表先于从表导入。改 mysql 数据库修改结构时如果加了字段、改了类型,记得触发器里的 INSERT 列如果有字段变化,触发器不会自动跟随表结构——改完表务必重新执行一次触发器创建脚本。这属于典型的“改了表忘了触发器”踩坑,查库存莫名不对时先看这里。

5.5 答辩演示翻车:本地跑得好好的,演示机上一动就 500

现象:在自己笔记本上运行一切正常,到答辩教室的电脑上页面也能打开,一登录或者一点提交就报 500,控制台提示 Access denied for user 或 Unknown database。

原因:演示机的 MySQL 服务可能没启动,或 root 账号密码与项目配置不同,或资料包里 SQL 脚本没在演示机上导入过。如果项目跑在你自己电脑上而演示机只开了浏览器投屏,那大概率是防火墙挡了 3306 端口或 Tomcat 没绑对外 IP。

解决:最稳的做法是把数据库和应用都放你本机,HDMI 线投屏演示,不要试图在教室电脑上重搭一套环境。出差前用命令行验证一遍:mysql 能连、SELECT 1 能返回、Tomcat 能访问首页、完整跑一遍登录到结账的流程。把 MySQL Windows 服务设为自动启动,防止演示前有人重启了机器导致服务没起来。血泪经验是演示当天早上提前二十分钟把项目跑起来,所有环节点一遍,确认没问题再去答辩,不要在演示台上临时改配置。

6. 从“能跑”到“高分”:答辩演示顺序和文档包装的最后一公里

6.1 演示顺序:先走业务流,再亮数据库

答辩时间通常只有五到十分钟,演示顺序决定了老师对你的第一印象。我见过太多人上来就打开数据库展示表结构,老师看得昏昏欲睡。正确的顺序是:先让老师看到系统能干什么,再让老师明白系统内部怎么设计的。登录页进管理员账号,加一个商品,录一张进货单把库存冲上去,切收银员账号开一张销售单完成结账小票,再回到库存列表看数字变化——这一串流程走下来,系统就活了。然后才打开 Navicat 或命令行,查 t_sale_item 里的数据和刚才录的单对上,再演示 v_low_stock 视图和按日销售统计 SQL。先业务后数据,老师不会觉得你在背稿。

演示步骤操作动作讲解要点
登录切换管理员和收银员账号角色权限控制怎么实现的
进货新增进货单并提交进货明细和商品库存联动
销售多商品结账事务保证三张表同时成功
库存核对数据库查询库存变化触发器自动扣减库存
统计展示日销售汇总GROUP BY 与视图设计

6.2 文档与细节:数据库修改结构、Git 记录与一个亮点页面

文档是“高分项目”这个 title 的另一半支撑。数据字典、ER 图和触发器说明这三样缺一不可。数据字典里每张表的每个字段都要一行说明,类型、是否为空、默认值、注释写全,这一步不加分但漏了必扣分。ER 图用 PowerDesigner 或 draw.io 画清楚实体间的一对多关系,连线上标注 1:N,视觉上要能看到六张主表的关系。触发器和视图的 SQL 文本放进文档附录,并在正文写明“为什么用触发器而不是代码扣库存”的设计理由。这三个东西准备好,答辩时你手里就有东西可指了。

一个极推荐的加分动作是给项目加一个近七天销售趋势页面,用 ECharts 画柱状图,数据来自第 3 章的按日统计 SQL。这个功能工作量不大,但视觉效果冲击力强,老师看一眼就觉得你的系统比别人完整。加之前先确认项目里有没有引入前端图表库,没有就从 CDN 引一个,页面只用 GET 接口拉 JSON 数据,别把图表搅进结账事务里。配套建议是进入项目目录就执行 git init,每次调整前提交一次,给这台“资料包老机器”留一份后悔药,改坏了能随时回到上一个能跑的版本——我当年把触发器删了又找不回原稿的那种痛,你就别吞一遍了。

这几点做完,剩下的就是把流程再顺两遍。前一个教训是,我大学交课设前一天往数据库里塞了上万条测试数据,第二天答辩演示“按天统计”页面卡了三秒,老师眉头一皱,分数就下去了。从那以后我就养成了习惯:演示前把数据导空,只留 50 条以内手工造的数据,每条商品名都起得规规矩矩,跑起来丝滑得跟新装系统一样。项目值不值得下载、方向值不值得投入,最终看的是你能不能靠这套东西把课设讲清楚、跑明白——资料再多,不如自己把一条进出货链路跑透。希望这些经验帮到你。

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

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进