news 2026/9/28 15:58:59

基于JSP+Servlet+JavaBean的超市进销存系统毕设全攻略

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于JSP+Servlet+JavaBean的超市进销存系统毕设全攻略

简介:这是一份基于 JSP+Servlet+JavaBean 架构的超市进销存管理系统项目,面向 JavaWeb 课程设计、毕业设计及入门提升学习者,适合需要完整可运行代码与配套配置说明的用户。系统围绕商品管理、分类维护、入库/出库记录、管理员与用户信息、数据导出等核心模块展开,MVC 分层清晰,Servlet 负责请求控制,JavaBean 封装业务数据,配合 MySQL 存储业务数据。资源整体为 zip 压缩包,共 134 个文件,大小约 6.94MB;包含 9 个 JSP 页面、14 个 Java 源码、14 个 class 编译产物及 6 个 jar 依赖包,前端由 CSS、JavaScript 和图片素材构成,并附带工程配置文件,按文档配置好环境后即可本地编译运行。项目难度适中,代码结构较规整,适合参考其登录控制、分页查询、数据导入导出等实现思路,也方便在此基础上进行功能扩展或二次开发。当前已有 67 人学习下载,有疑问也可向作者咨询。

1. 这套 JSP+Servlet+JavaBean 进销存系统,到底帮毕设解决了什么

一个很现实的现象:每年到三四月,JavaWeb 方向的毕设需求里,进销存管理系统的出现频率高得惊人。超市要管商品、供应商、进货单、销售单、库存余量,这套业务边界清晰,表结构不复杂,正好卡在“课程设计往上够不着、纯管理系统又太简单”的中间地带。而 JSP + Servlet + JavaBean 的组合,恰恰是 JavaWeb 最经典的三层分离写法——浏览器请求打到 Servlet,Servlet 调 JavaBean(业务和数据封装),结果渲染回 JSP 页面。

这篇笔记就是把“基于 JSP+Servlet+JavaBean 的超市进销存管理系统”这个压缩包项目拆开讲清楚:它实际包含哪些模块,代码怎么组织,数据库表怎么建,部署时哪些地方最容易翻车。适合三类人看:正在做这类毕设、需要把系统讲明白好答辩的在校生;刚接触 JavaWeb、想用一个小项目把三层架构从理论落成代码的初学者;以及接手这类老项目、需要快速梳理结构和改 bug 的维护者。全程按“先立结构、再写代码、最后填坑”的顺序走,照着做能跑出一个完整可演示的系统。

2. 先把三层骨架切开:JavaBean 管数据,Servlet 当调度,JSP 只做展示

2.1 为什么要拆三层:这是能答辩、也好改代码的最小结构

JSP + Servlet + JavaBean 的组合,本质是 MVC 在教学项目里的简化落地。Model 是 JavaBean,负责封装商品、订单、供应商这类业务数据和简单的业务方法;View 是 JSP,只负责把数据展示出来;Controller 是 Servlet,接收请求、调业务方法、决定跳转到哪个页面。这个拆分最大的意义不是“优雅”,而是出问题的时候你知道去哪一行改代码。

举一个最容易碰到的场景:商品列表价格显示多了两位小数,或者是把“元”显示成了“分”。如果你把数据库查询和 HTML 渲染全混在同一个 JSP 里,你得在几百行的<% %>里找那个字段的取值位置;如果拆了层,问题只可能出现在三个地方——JavaBean 里 price 字段的类型、Servlet 里查询数据时的封装方式、JSP 里格式化输出的表达式。定位范围一缩,修复就快了。

这套结构也很适合在答辩时讲:老师问“你的代码怎么组织的”,你就把三层各指一下,再说一句“Servlet 不写 SQL,JSP 不写业务逻辑,JavaBean 封装数据和操作”,这个回答比“我用的 MVC”要扎实得多,因为你能指到具体的类和具体的落盘文件上。

2.2 包结构设计:先定好人名,后面写代码不迷路

常见做法是按“实体 + 数据访问 + 控制 + 工具”四块建包,我一般用下面这套结构:

src/main/java ├── com.shop.entity // JavaBean,对应数据库表 │ ├── Goods.java │ ├── Supplier.java │ └── User.java ├── com.shop.dao // 数据访问层,负责 JDBC 操作 │ ├── GoodsDao.java │ ├── SupplierDao.java │ └── UserDao.java ├── com.shop.servlet // Servlet,接收请求、调 DAO、跳页面 │ ├── LoginServlet.java │ ├── GoodsListServlet.java │ └── SaleServlet.java └── com.shop.util // 工具类:数据库连接、字符编码过滤器 ├── DBUtil.java └── EncodingFilter.java

有两点要建议你从一开始就遵守。一是命名宁长勿短,GoodsListServlet比GoodsServlet的语义清楚,因为后者既可能是列表也可能是新增;二是不要出现Test1.java、Demo2.java这种名字,这类文件在毕设答辩时一旦被老师点到,解释成本极高。

如果你用的开发环境是 Maven 项目,src/main/webapp下面放 JSP 页面,常见的分法是:

src/main/webapp ├── login.jsp ├── goods │ ├── list.jsp │ └── edit.jsp └── sale └── index.jsp

这样按页面归属功能分目录,比全部平铺在 webapp 根目录更清晰,也方便后面配置 Servlet 的 url-pattern 时跟 JSP 路径对应起来。

2.3 核心 JavaBean 怎么写:字段类型先想清楚

这里以商品Goods类为例子。JavaBean 有一套约定俗成的写法:属性私有、提供 getter/setter、必须有一个无参构造方法。这三个特征几乎决定了它在 JSP 里能不能被 EL 表达式直接取到值,也决定了 DAO 层能不能方便地封装它。看代码:

package com.shop.entity; import java.math.BigDecimal; import java.util.Date; public class Goods { private Integer id; // 商品ID,主键自增 private String name; // 商品名称 private String category; // 分类:饮料、食品、日用品 private BigDecimal price; // 单价,用 BigDecimal 而不是 double private Integer stock; // 当前库存 private Integer supplierId; // 关联供应商ID private Date createDate; // 建档日期 public Goods() { } // getter 和 setter 省略,需自行补全 // 典型写法:public Integer getId() { return id; } // public void setId(Integer id) { this.id = id; } }

重点说price字段的选择。数据库里价格如果用DECIMAL(10,2),Java 这边对应的最佳类型就是BigDecimal。很多初学者习惯用double,看着没毛病,但超市商品一旦算总价、算折扣、算库存金额,double的浮点误差会在第二个小数点以后露出来,做演示时打出19.999999这类数非常尴尬。使用BigDecimal也算一个可以写进文档的技术点:精度可控、进制安全。

另一个容易忽略的字段是stock。进货和销售操作都会读它、改它、写回它,这是一个典型的并发数据,课程设计阶段可以不考虑多线程竞争,但代码里要给这个字段留一个独立的更新方法,后面验证库存闭环时会用到。

2.4 Servlet 在哪个位置干活:生命周期与请求路径的约定

Servlet 是被容器(Tomcat)管理的 Java 类,它的整个生命周期就三个关键方法:init()、service()、destroy()。init()在第一次被请求或容器启动时调用一次,适合做初始化工作;service()每次请求都执行,实际开发里一般不直接重写它,而是重写doGet()和doPost(),让框架替你把请求分发到对应方法;destroy()在应用卸载时收尾。

写一个登录 Servlet 看整个链条:

package com.shop.servlet; import com.shop.dao.UserDao; import com.shop.entity.User; 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 request, HttpServletResponse response) throws ServletException, IOException { // 统一设置请求编码,防止中文用户名乱码 request.setCharacterEncoding("UTF-8"); String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/goods/list"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); } } }

这段代码包含了这个项目里最常用的三个 Servlet API 用法:@WebServlet("/login")用注解配置访问路径,省掉 web.xml 里一大段<servlet>映射配置;request.getSession()拿到当前会话;response.sendRedirect()和forward()是两个跳转方式,前者是浏览器重新发起请求,地址栏会变,后者是服务端内部转发,地址栏不变。登录成功用重定向,目的是防止用户按 F5 刷新时重复提交登录表单;失败用转发,因为需要把error这个错误提示带到 JSP 页面里。

3. 从建表 SQL 到商品列表页:一条能跑通的最小闭环

3.1 四张业务表:user、goods、supplier、stock_log 怎么建合理

进销存的核心是“货进出有记录”,只建一张商品表没法体现进货和销售的变化过程。课程设计规模下,四张表足够覆盖核心业务:用户表管登录,商品表管当前库存,供应商表管进货来源,库存流水表管每一次数量变动。这样既能展示进货单和销售单,也能回答“某个商品的库存是怎么变成现在这个数的”这类答辩追问。

CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL, `password` VARCHAR(50) NOT NULL, `real_name` VARCHAR(50) DEFAULT '', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `supplier` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `contact` VARCHAR(50) DEFAULT '', `phone` VARCHAR(20) DEFAULT '', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `goods` ( `id` INT NOT NULL AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `category` VARCHAR(50) DEFAULT '', `price` DECIMAL(10,2) NOT NULL DEFAULT 0.00, `stock` INT NOT NULL DEFAULT 0, `supplier_id` INT DEFAULT NULL, `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_supplier` (`supplier_id`), CONSTRAINT `fk_goods_supplier` FOREIGN KEY (`supplier_id`) REFERENCES `supplier`(`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `stock_log` ( `id` INT NOT NULL AUTO_INCREMENT, `goods_id` INT NOT NULL, `change_type` VARCHAR(10) NOT NULL COMMENT 'IN=进货,OUT=销售', `quantity` INT NOT NULL COMMENT '变动数量,正数', `before_stock` INT NOT NULL, `after_stock` INT NOT NULL, `operator_id` INT DEFAULT NULL, `create_date` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_goods` (`goods_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

字段设计上有两个关键决策。一个是库存流水表记录了before_stock和after_stock,不是为了好看,而是为了对账——当库存数和流水算出来的结果不一致时,能定位是哪一笔操作出了问题。另一个是商品表用外键关联供应商,某些课设项目为了省事不建外键,只保留一个 supplier_id,我建议这个项目里把外键建上,因为供应商模块本身就是进销存的组成部分,外键能在数据库层面防止录入不存在的供应商 ID。MySQL 上选择 InnoDB 引擎是因为它支持外键和事务,后者在扣减库存时非常关键。

3.2 DAO 层写法:加载驱动、获取连接、查询与封装的固定套路

DAO 层的代码模式几乎是固定的:获取连接、写 SQL、用 PreparedStatement 占位符、执行、遍历结果集、封装成 JavaBean、关闭资源。这里不为了“优雅”引入 MyBatis 或 JdbcTemplate,因为 JSP+Servlet 的项目阶段,手写 JDBC 是理解数据访问过程最直接的方式,也方便答辩时解释每一行的作用。

数据库连接的工具类建议单独放,避免每个 DAO 里重复写加载驱动的代码:

package com.shop.util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(ResultSet rs, PreparedStatement ps, Connection conn) { try { if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps != null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn != null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }

这段代码里值得注意的参数,全部集中在 JDBC URL 上:useUnicode=true和characterEncoding=utf8一起出现,才能保证中文参数从 Java 这边进 MySQL 不乱码;serverTimezone如果你用的 MySQL 8.x 驱动,最好在 URL 里追加serverTimezone=Asia/Shanghai,否则驱动校验时会报时区错误。

看一个完整的商品 DAO 查询方法:

package com.shop.dao; import com.shop.entity.Goods; import com.shop.util.DBUtil; import java.math.BigDecimal; import java.sql.*; import java.util.ArrayList; import java.util.List; public class GoodsDao { public List<Goods> findAll() { List<Goods> list = new ArrayList<>(); String sql = "SELECT id, name, category, price, stock, supplier_id, create_date FROM goods"; Connection conn = null; PreparedStatement ps = null; ResultSet rs = null; try { conn = DBUtil.getConnection(); ps = conn.prepareStatement(sql); rs = ps.executeQuery(); while (rs.next()) { Goods g = new Goods(); g.setId(rs.getInt("id")); g.setName(rs.getString("name")); g.setCategory(rs.getString("category")); g.setPrice(rs.getBigDecimal("price")); g.setStock(rs.getInt("stock")); g.setSupplierId(rs.getInt("supplier_id")); g.setCreateDate(rs.getTimestamp("create_date")); list.add(g); } } catch (SQLException e) { e.printStackTrace(); } finally { DBUtil.close(rs, ps, conn); } return list; } }

这里有个细节值得养成习惯:SELECT后面显式列出字段名,不用SELECT *。两者在这个查询里结果一样,但显式列字段能让你清楚地看到 JavaBean 里每个属性对应着表的哪个列,一旦表结构改了,代码编译期就能暴露问题。rs.getBigDecimal("price")和rs.getTimestamp("create_date")是类型对应的关键,如果 JavaBean 里是BigDecimal而这里用rs.getDouble(),JSP 页面的金额格式就会乱。

3.3 一个能显示商品列表的 Servlet + JSP:路径怎么对起来

有了 DAO 之后,把商品列表展示出来只需要一个 Servlet 和一个 JSP 页面。Servlet 负责查数据、把数据塞进 request、再转发给 JSP;JSP 负责用 JSTL 和 EL 表达式把集合渲染成表格。注意 JSTL 标签库需要单独引入依赖,这个在部署阶段是新手最常见的问题之一,后面会专门讲。

先写 Servlet:

package com.shop.servlet; import com.shop.dao.GoodsDao; import com.shop.entity.Goods; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.*; import java.io.IOException; import java.util.List; @WebServlet("/goods/list") public class GoodsListServlet extends HttpServlet { private GoodsDao goodsDao = new GoodsDao(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { List<Goods> goodsList = goodsDao.findAll(); String keyword = request.getParameter("keyword"); // 如果带了 keyword 参数,则做模糊过滤,这个简单功能很加印象分 if (keyword != null && !keyword.trim().isEmpty()) { // 常见做法:在 DAO 里加一个 findByNameLike 方法,这里调用后替换 goodsList } request.setAttribute("goodsList", goodsList); request.getRequestDispatcher("/goods/list.jsp").forward(request, response); } }

再写 JSP 的列表主体:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head> <title>商品列表</title> </head> <body> <h2>商品列表</h2> <table border="1"> <tr> <th>ID</th> <th>名称</th> <th>分类</th> <th>单价</th> <th>库存</th> <th>操作</th> </tr> <c:forEach items="${goodsList}" var="goods"> <tr> <td>${goods.id}</td> <td>${goods.name}</td> <td>${goods.category}</td> <td>${goods.price}</td> <td>${goods.stock}</td> <td> <a href="${pageContext.request.contextPath}/goods/edit?id=${goods.id}">编辑</a> <a href="${pageContext.request.contextPath}/sale?id=${goods.id}">销售</a> </td> </tr> </c:forEach> </table> </body> </html>

两个关键路径需要注意。Servlet 的访问路径是/goods/list,JSP 展示用的request.getRequestDispatcher("/goods/list.jsp")是服务端内部转发路径,它表示 webapp 目录下的真实文件位置;“编辑”和“销售”链接里用${pageContext.request.contextPath}拼接项目上下文路径。如果你直接写/goods/edit?id=1而不是${pageContext.request.contextPath}/goods/edit?id=1,部署时项目名一旦变化,所有链接都会 404,这就是俗称的路径“写死了”。

3.4 登录与 Session:让系统真正“认识”当前操作者

进销存系统不能谁都能进来改数据,登录功能是商品列表之外最先要做的模块。前面已经写了LoginServlet和UserDao的代码,这里补一个关键设计:登录成功后把用户对象放进 Session,后续的进货、销售操作都从 Session 里取loginUser,记录到流水表的operator_id字段。

这正好呼应热搜词里的“servlet 生命周期”——Session 在 Servlet 容器里是独立于 Servlet 实例存在的,Servlet 是单实例多线程处理请求,但每个浏览器会话持有独立的 Session 对象。所以你不能把“当前登录用户”存在 Servlet 的成员变量里,那会出现 A 登录后看到 B 的用户名这种串号事故。稍微想一想就能明白:Servlet 的成员变量被所有请求共享,而 Session 是每个浏览器私有的。

// 在某个需要登录的 Servlet 里取当前用户 HttpSession session = request.getSession(false); if (session == null || session.getAttribute("loginUser") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; }

一个完整的系统通常会把这个校验逻辑写成过滤器(Filter),统一配置在需要保护的路径上,而不是在每个 Servlet 里复制这段代码。但课程设计阶段,先写在每个 Servlet 里也不丢人——运行起来逻辑清晰,答辩时能讲明白 Session 是干什么的就行了。

4. 四个最容易翻车的地方:编码、部署、字段类型和 Session 陷阱

4.1 现象:页面中文全是问号乱码,数据库里存的中文变成 ???

原因是编码链路上某一环断了。一个请求从浏览器到 Tomcat、再到 JSP 显示,至少经过四个环节:页面本身的编码、请求参数编码、数据库连接编码、数据库表编码。常见问题是 JSP 文件没写pageEncoding、request.setCharacterEncoding("UTF-8")只写了 POST 路径而 GET 参数没处理、MySQL 表建成了latin1。

解决方法是把编码一次性铺满:JSP 文件头部写全<@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8"%>;在web.xml里配置一个CharacterEncodingFilter,拦截所有请求强制设成 UTF-8;数据库连接 URL 带useUnicode=true&characterEncoding=utf8;建表语句明确写CHARSET=utf8mb4。这四处都检查过一遍,中文乱码的问题就不会再出现。特别是表字符集,改表的编码是很多人漏掉的最后一环。

4.2 现象:执行 SQL 时报“Data truncation: Out of range value for column”

原因是字段类型和写入数据不匹配。一种常见场景是商品价格:数据库里定义DECIMAL(10,2),应用程序里用字符串拼接 SQL 往里写一个很大的数,或者用了INT存价格导致 12.5 直接被截断成 12。另一种常见场景是库存数量:进货单填了 10000,但表字段是TINYINT,最大值只有 127,写入直接报错。

解决方法是先搞清楚每个字段的业务量级再定类型。价格统一用DECIMAL(10,2),Java 侧对应BigDecimal;库存用INT,在 Java 侧用Integer接收;数量字段入库前做逻辑校验——进货数量不能为负数,销售数量不能超过当前库存。这种校验最好放在 Servlet 层,用简单判断拦截非法请求,别指望数据库的报错信息反馈给用户。

4.3 现象:IDEA 里点运行一切正常,打成 war 包部署到 Tomcat 就 404

原因是路径感知差异。在 IDEA 里集成 Tomcat 运行时,IDEA 会帮你自动处理 context path,你能访问/goods/list不代表你部署到独立 Tomcat 的webapps目录下还能用同样的路径访问。如果 war 包名字是shop.war,部署后所有 URL 都要加/shop前缀;有的新手随手把 war 包改成ROOT.war,虽然能去掉前缀,但和容器默认路径之间的关系常常被忽略。

解决方法是养成写相对路径的习惯:重定向用response.sendRedirect(request.getContextPath() + "/goods/list");页面里的链接全部用${pageContext.request.contextPath}拼前缀。这样无论是部署成shop.war还是ROOT.war,链接都会跟着上下文路径走,不会再出现换了个部署方式就大面积 404 的情况。如果你负责维护老项目,看到页面上有一堆手写的/shop/goods/list,那基本就是踩了这个坑。

4.4 现象:销售一次商品,刷新页面后库存扣了两次,流水多了两条

原因是用户按 F5 导致浏览器重新提交了上一个 POST 请求。销售操作在SaleServlet的doPost里扣减库存、写流水表,用户付款后按刷新,浏览器会重放这个 POST,库存和流水就被重复记录了。

解决方法是“Post/Redirect/Get”模式:doPost里处理完业务后不要直接forward到成功页面,而是用sendRedirect跳到一个 GET 地址,比如response.sendRedirect(request.getContextPath() + "/sale/success")。这样浏览器最后一次请求是 GET,刷新时只会重放 GET,不会再重新执行一次扣库存逻辑。这也是前面 LoginServlet 里登录成功用sendRedirect而不是forward的原因。除了重定向之外,销售扣减库存建议配合事务——扣库存和写流水两条 SQL 必须在一个事务里,要么都成功要么都失败,否则会出现库存扣了但流水没记的脏状态。手写 JDBC 时用conn.setAutoCommit(false)、最后conn.commit(),已经足够应对这个场景。

5. 从能用走到能答辩:三个验证脚本和一个进阶思路

5.1 先用 curl 把登录接口打一遍,别急着开浏览器

项目部署起来后,第一件事不是点页面,而是用 curl 验证最核心的接口链路。这样能把“前端页面问题”和“后端接口问题”分开排查——页面渲染可以后面慢慢调,接口必须一次通。

# 1. 登录,保存 Cookie 到本地文件 curl -c /tmp/shop_cookie.txt -X POST \ -d "username=admin&password=123456" \ -w "%{http_code}" \ http://localhost:8080/shop/login # 2. 带 Cookie 请求商品列表接口 curl -b /tmp/shop_cookie.txt \ -w "\n%{http_code}" \ http://localhost:8080/shop/goods/list

第一个命令返回302表示登录成功、重定向到列表页;第二个命令能拉到商品列表的 HTML 就说明登录校验和列表查询都正常。如果第一个命令直接返回200且内容是登录页,说明登录逻辑没走通,此时检查UserDao的 SQL 和密码比对逻辑。

5.2 验证库存闭环:手动造一笔进货和销售,看库存和流水对得上

用一个 MySQL 客户端直接模拟数据库操作,验证库存和流水的对应关系。这是最直接的业务正确性验证。

-- 假设商品ID=1,当前库存为50 -- 模拟一笔进货:进 20 件 INSERT INTO stock_log (goods_id, change_type, quantity, before_stock, after_stock, operator_id) VALUES (1, 'IN', 20, 50, 70, 1); UPDATE goods SET stock = 70 WHERE id = 1; -- 模拟一笔销售:卖出 15 件,库存 70 -> 55 INSERT INTO stock_log (goods_id, change_type, quantity, before_stock, after_stock, operator_id) VALUES (1, 'OUT', 15, 70, 55, 1); UPDATE goods SET stock = 55 WHERE id = 1; -- 核对:按流水累计变动的库存是否等于当前商品表的库存 SELECT g.stock AS current_stock, (SELECT before_stock FROM stock_log WHERE goods_id = 1 ORDER BY id ASC LIMIT 1) + SUM(CASE WHEN change_type='IN' THEN quantity ELSE -quantity END) AS calc_stock FROM goods g WHERE g.id = 1;

这个验证技巧在答辩时特别加分:你不仅做了功能,还想到了“库存对账”。一旦两个数字对不上,就能定位是 UPDATE 语句漏了,还是流水记录写错了。平时开发时,每次改完库存相关代码就跑一遍这个核对,能提前拦住大部分隐藏 bug。

5.3 从课程设计走向真实项目的三个扩展点

如果你想在这个项目上继续往前走,方向其实很明确。第一个是 JSP 这项技术的“终局”问题——搜索引擎里经常有人问“nginx 支持 jsp 吗”,答案是不支持,nginx 只能处理静态资源,动态请求要通过proxy_pass反向代理到 Tomcat,让 Tomcat 里的 JSP/Servlet 去处理,这个知识点在部署生产环境时必然碰到。第二个是换成 Spring Boot 后 JSP 目录的安置问题——src/main/resources下放不了 JSP,JSP 必须放在src/main/webapp,且 Spring Boot 内置 Tomcat 只支持将 JSP 打包进 jar 的META-INF/resources目录,实际项目里更多是用模板引擎替代 JSP;第三个是 AdminLTE 或 Bootstrap 这类前端框架的引入——让你的系统页面不再像 2008 年的风格。

这套项目的价值在于,它用最少的依赖(只有 Tomcat + MySQL + JDBC),把 JavaWeb 底层的核心机制完整走了一遍。Servlet 的生命周期、Session 的会话管理、PreparedStatement 的参数绑定、JSP 的 EL 表达式,这些知识换到任何 JavaWeb 框架里都还在。我做了这么多年项目,见过太多上来就学 Spring Boot、结果连 HTTP 请求到方法调用这条链路都讲不清的人——先把这套老项目吃透,后面的路反而好走得多。希望这份笔记能帮你的毕设少踩几个坑,做得顺手一点。

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

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

Console线选购与排障:CH340与FTDI芯片详解

第一次拿着新买的Console线去调试设备&#xff0c;在机房里蹲了半小时插上USB&#xff0c;电脑一点反应都没有。这种经历我相信不少网络工程师都遇到过。不是你的线坏了&#xff0c;大多数时候是芯片驱动和系统之间在打架&#xff0c;尤其是CH340和FTDI这两类芯片&#xff0c;处…

作者头像 李华
网站建设 2026/9/28 15:56:02

AI日报自动化生产全解析:从信息采集到智能分发的工程实践

1. 一份AI日报的诞生&#xff1a;从信息洪流到结构化洞察每天早上七点&#xff0c;我的信息采集脚本准时跑完最后一轮抓取&#xff0c;数据库里躺着过去24小时内新增的四百多条AI相关动态。这些内容来自技术社区、产品发布页、学术预印本平台、行业媒体和开发者论坛&#xff0c…

作者头像 李华
网站建设 2026/9/28 15:53:47

微信开源RAG知识库引擎:本地部署、混合检索与引用溯源的实践指南

先问一个问题&#xff1a;你电脑里是不是也存了一堆PDF、Word、Markdown&#xff0c;真到用的时候一个都找不到&#xff1f;微信最近开源的那个知识库项目&#xff0c;就是冲着这个痛点去的。我第一次在GitHub刷到这个项目时还挺意外——仓库里没有花哨的宣传图&#xff0c;就是…

作者头像 李华
网站建设 2026/9/28 15:53:33

Multisim 14.0中用74LS160/161搭建61进制计数器完整指南

上周有位学弟拿着数字电路课设题来问&#xff1a;在Multisim 14.0里&#xff0c;用74LS160和74LS161搭一个61进制计数器&#xff0c;怎么总是出不来效果。他按网上的电路连了一遍&#xff0c;仿真一运行&#xff0c;两个数码管不是从乱码开始跳&#xff0c;就是一路冲到99。我相…

作者头像 李华
网站建设 2026/9/28 15:52:52

大模型毫秒级响应是伪命题?从流式输出到推理加速的实战解析

在和大模型打交道的这段时间里&#xff0c;我遇到过最多的一个误解&#xff0c;就是把“流畅体验”直接等同于“毫秒级接口响应”。真实用户看到的是&#xff1a;光标转了几圈之后&#xff0c;答案开始一个字一个字冒出来&#xff0c;有时候先蹦出来的是一个“好的”&#xff0…

作者头像 李华