前几天有人私信我,说自己毕业设计抽中了“超市进货管理系统”,题目要求基于JSP,问我第一步到底该干什么。这问题我太熟了,每年毕业季都会遇到好几个选这类管理系统的学生,JSP+超市进货、JSP+学生信息、JSP+图书管理,本质上都是一个套路。但很多人拿到题目后依然懵:JSP会写一点,超市进货也有概念,可这两个东西怎么组装成一个能演示、能答辩的系统,脑子里完全没画面。
这篇文章就围绕“JSP超市进货管理系统”把完整思路走一遍,从需求梳理、技术选型、数据库设计、核心流程代码,到答辩会被追问的问题,全部覆盖。它不是照着某份开源代码念,而是我给自己带的人做这个项目时实际用的那套打法。如果你选的就是这个题目,或者类似的管理系统题目,这篇文章可以当一份路线图来用。
1. 开题别急着写代码:先把进货业务倒推一遍
拿到一个管理系统的题目,第一反应是上网找源码,这没错。但如果你自己对“进货”这件事的业务没有概念,就算把源码放你面前,你也不知道该改哪里、该加哪里。所以我建议先花半天时间把业务理顺,这一步往往能决定你后面是轻松还是天天返工。
1.1 从一个采购员的工作场景倒推功能
想象你在经营一家社区超市,货架上某款饮料快卖完了,接下来会发生什么?
首先是发现需求,要么是店员巡店看到快空了,要么是系统提示某商品低于库存下限。然后是找供应商,查一下之前合作过的饮料批发商,打电话或者发消息确认价格、起订量、供货周期。对方报价没问题,订单就算确定了。等货送到门口,仓库的人要拿着送货单清点,看数量对不对、包装有没有破损,没问题就登记入账,商品库存随之增加,钱记到供应商往来账上。
把这个流程翻译成系统功能,就是三件核心事:商品档案要维护,进货订单要从无到有地走状态,到货之后要入库并且让库存变化。围绕这三件事再展开,就是供应商管理、订单管理、入库管理、库存查询,加上系统层面的用户登录和角色权限。想清楚这条链路,就够了。
1.2 功能模块全景图和优先级判定
我把常用模块整理成一张表,对应的优先级也标了出来,写代码的时候按优先级来,不必一上来就追求全功能。
| 模块 | 包含功能 | 优先级 |
|---|---|---|
| 系统登录 | 登录验证、退出登录、修改密码 | 必需 |
| 供应商管理 | 供应商的增删改查、联系人、电话、状态 | 必需 |
| 商品管理 | 商品档案维护、分类、规格、进价售价、库存预警阈值 | 必需 |
| 进货订单 | 创建订单、提交审核、审核通过/驳回、订单查询 | 必需 |
| 入库管理 | 到货登记、入库单生成、库存自动增加 | 必需 |
| 库存管理 | 库存查询、库存预警、入库流水查询 | 必需 |
| 用户管理 | 用户维护、角色分配 | 加分 |
| 进货统计 | 按供应商、按商品的进货额统计 | 加分 |
核心主链路就是“商品档案 -> 创建采购单 -> 审核 -> 入库 -> 库存更新 -> 库存预警”。其它功能再怎么加,都不影响系统骨架,先把主链路跑通,再做加分项。
1.3 角色权限的设计:别搞成只有管理员
很多学生做管理系统,最后只做一个管理员账号,所有页面都能进,所有按钮都能点。这样也能交差,但答辩时很容易被问住:“那这个系统的权限控制体现在哪?”
我的建议是至少拆三个角色:管理员、采购员、仓管员。采购员负责创建进货订单和查询,仓管员负责到货入库操作,管理员拥有全部权限并且负责订单审核。角色权限的落地方式不复杂,用户表里加一个role字段,写一个过滤器,拦截请求后会判断当前登录用户的角色,没有权限直接跳转到无权限页面。
这样既体现了设计思考,代码量增加也不大。这里埋一个伏笔,后面讲核心流程时会具体展开。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型定调:JSP这条技术路线该怎么搭
题目要求用JSP,那就不需要纠结要不要上Spring Boot,因为一旦脱离JSP做前后端分离,题目就偏了。JSP在这里不是技术包袱,它本身就是一种合格的视图层方案,尤其对于这类CRUD为主的进销存系统。
2.1 经典组合:JSP + Servlet + JavaBean + MySQL
我给这个项目推荐的技术栈是:JDK 1.8,Tomcat 9,MySQL 5.7或8.0,JSP/Servlet原生搭配JavaBean和DAO,数据库连接池用Druid或c3p0,前端用Bootstrap加jQuery,页面渲染用JSTL和EL表达式。
每个组件负责什么,要能一句话说清楚:JSP负责页面展示,Servlet负责接收请求和跳转控制,JavaBean对应数据实体,DAO负责和数据库交互。这就组成了一个非常标准的MVC结构,JSP是View,Servlet是Controller,JavaBean和DAO加在一起承担Model的角色。
为什么要坚持这套老组合?因为管理系统的核心是业务闭环,不是框架炫技。JSP加Servlet这套组合对课堂知识的覆盖很饱满,老师问架构、问MVC、问HTTP请求生命周期,你都能讲得很清晰。
2.2 项目目录结构怎么搭才不会乱
我见过不少学生把所有JSP页面堆在webapp根目录,Servlet也全部塞在一个类里,数据库操作写在JSP里,最后系统运行没问题,但代码根本没办法维护。目录结构建议这样划分:
code复制src
├── com.supermarket
│ ├── filter
│ │ └── LoginFilter.java
│ ├── servlet
│ │ ├── LoginServlet.java
│ │ ├── SupplierServlet.java
│ │ ├── GoodsServlet.java
│ │ ├── OrderServlet.java
│ │ └── StockInServlet.java
│ ├── service
│ │ └── OrderService.java
│ ├── dao
│ │ ├── SupplierDao.java
│ │ ├── GoodsDao.java
│ │ └── OrderDao.java
│ ├── entity
│ │ ├── Supplier.java
│ │ ├── Goods.java
│ │ ├── PurchaseOrder.java
│ │ └── OrderItem.java
│ └── util
│ └── DBUtil.java
webapp
├── admin
│ ├── login.jsp
│ ├── supplier_list.jsp
│ ├── goods_list.jsp
│ ├── order_add.jsp
│ ├── order_list.jsp
│ ├── order_audit.jsp
│ └── stock_in.jsp
├── static
│ ├── css
│ ├── js
│ └── images
└── WEB-INF
└── web.xml
Servlet就用一个类对应一个业务对象,不要所有请求都堆在一个Servlet里,用method参数区分。这样写的好处是定位问题很快,答辩时老师看你代码结构清晰,印象分会好不少。
2.3 为什么我不建议你在这个项目里强行上框架
有学生认为加个Spring Boot会让项目显得高级,但我的看法不同。毕业设计的评分标准里,“系统能正常运行”和“核心业务逻辑清晰”比“用了多少框架”重要得多。如果你选择Spring Boot,那JSP基本只能作为模板引擎存在,传统JSP的页面方式会变得别扭,而且Spring Boot本身要配置的依赖和自动装配概念可能比系统本身还难讲清楚。
还有一个很实际的考虑:答辩老师大概率会顺着你使用的技术往下问。你用Spring Boot,老师就会问依赖注入、自动配置、starter原理,你答不上来反而是减分项。用JSP+Servlet,问题基本围绕你的业务代码,你完全可以掌控。
2.4 Tomcat版本选择:javax和jakarta之间的坑
这个坑几乎每年都会坑到学生。Java Servlet的包名在Tomcat 10之后发生了重大变化,原来的javax.servlet变成了jakarta.servlet。如果你用了Tomcat 10或更高版本,去运行网上大量基于Servlet 4.0及以下的老教程代码,启动时就会直接报类找不到或者ClassCastException。
解决办法很简单:固定使用Tomcat 9或者Tomcat 8.5,网上那些旧代码可以直接运行。这个细节虽然不起眼,但能让你少折腾一个晚上。另外JDK版本也建议用8或11,太新的JDK配合老Tomcat也可能出现兼容问题。
3. 数据库先行:用六张核心表撑起进货闭环
管理系统项目的核心通常不在代码,而在表结构设计。表设计得好,业务就顺;表设计得乱,后面写代码会处处难受。
3.1 核心表设计总览
围绕进货业务,我设计为六张核心表,每张表各司其职:
| 表名 | 用途 |
|---|---|
| sys_user | 系统用户,存储账号、密码、角色 |
| supplier | 供应商档案,存名称、联系人、电话、状态 |
| goods | 商品档案,存名称、分类、规格、进价、售价、库存 |
| purchase_order | 进货订单主表,存订单号、供应商、总金额、状态 |
| purchase_order_item | 进货订单明细表,存每个商品在下单时的单价和数量 |
| stock_in_record | 入库流水表,记录每一次进货入库的商品和数量 |
这几张表的关系可以这样理解:用户下订单,订单一定属于某个供应商,一个订单包含多个商品明细;到货后,订单商品进入库存,同时留下入库流水。用户表是独立体系,与业务表通过操作人字段关联。
3.2 订单主表和明细表为什么必须拆开
新手设计表最容易踩的坑,是把一个订单里的多个商品塞进一个字段,比如存成字符串“可乐x10,薯片x20”,或者给一个订单只留一个商品字段。这都是不可行的。
订单和商品是典型的一对多关系。一张进货单会包含多种商品,如果必须拆成多个订单,无形中增加了操作成本;如果塞到一个字段里,那查询汇总、入库追溯、金额统计都成了笑话。正确的做法是拆成主表和明细表,主表存订单的公共信息,比如订单号、供应商、总金额、状态;明细表存每个商品的单价、数量、金额,通过外键order_id关联。
类比一下,主表就是你在超市买完东西拿到的那张小票,明细表就是小票上逐行列出的商品清单。小票丢了,光看清单不知道总数;清单没了,光有小票也看不出买了哪些东西。
3.3 库存余额和流水记录为什么同时保留
商品表里有一个stock字段表示当前库存,同时库存变化又要记录到流水表,这两者是不是重复了?并不是。goods.stock是“余额”,它解决的是页面展示和判断是否缺货的问题,每次查询都不需要做SUM聚合,SQL简单高效。stock_in_record是“流水”,它解决的是追溯问题,某天进了多少货、为什么库存从100变成了150,通过流水可以查清楚。
这就像银行账户里的余额和银行流水的关系,余额告诉你现在有多少钱,流水告诉你每一笔钱是怎么来的。一个正经的进销存系统,这两者缺一不可。答辩时能讲出这一点,老师会觉得你不仅仅是在堆代码,而是理解了系统设计的本质。
3.4 表字段设计里的几个细节
金额字段永远用DECIMAL(10,2),不要用float或者double,浮点数在计算机里是二进制存储,算金额会产生精度误差,查一下0.1 + 0.2就知道了。库存字段用INT,不要用VARCHAR。订单号建议用时间戳加随机数,比如20250522103000,方便阅读和排序。每张表统一加上status字段,用来做逻辑删除,避免真正执行DELETE导致历史数据丢失。
供应商表里建议加status标记,如果和某家供应商终止合作,不要删除档案,而是把状态置为停用。因为历史订单里还挂着他的名字,删除后关联关系就断了。这些细节虽然不是核心功能,但能让你的系统更接近真实企业的需求。
4. 核心流程落地:从建订货单到库存增加的完整链路
整个系统最有技术含量的部分,是进货订单从创建、审核到入库的完整流程。这个流程如果跑通了,系统的骨架也就立起来了。
4.1 登录校验与权限拦截:过滤器是最优雅的方案
用户登录后,把用户对象放进session,后续每个请求都需要判断用户是否已经登录。如果一个一个Servlet去写判断,既重复又容易漏。更合理的方式是写一个LoginFilter,在web.xml里配置好拦截规则,访客未登录时一律重定向到登录页。
过滤器的逻辑大概是这样:
java复制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);
Object user = session == null ? null : session.getAttribute("user");
if (user == null) {
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
chain.doFilter(req, resp);
}
角色权限控制可以在过滤器里再加一层判断,比如订单审核的Servlet只允许管理员访问。如果当前用户角色不是管理员,就跳转到一个“没有权限”的提示页面。这一套下来,权限控制就完整了。
4.2 创建进货订单:主表和明细表要在同一个事务里写
创建订单的业务逻辑是:前端页面选择供应商,然后逐个选择商品、填写采购数量,点击提交后,后端同时完成两件事:插入一条订单主表记录,再批量插入属于这个订单的明细记录。
这两步必须在一个数据库事务里完成。因为如果主表插入成功,明细插入失败,就会出现一个没有商品明细的“空订单”。下面的代码展示了Service层如何控制事务:
java复制public boolean createOrder(PurchaseOrder order, List<OrderItem> items) {
Connection conn = null;
try {
conn = DBUtil.getConnection();
conn.setAutoCommit(false);
// 插入主表,拿到自增主键orderId
int orderId = orderDao.insertOrder(conn, order);
// 循环插入明细表,业务表order_id关联
for (OrderItem item : items) {
item.setOrderId(orderId);
orderDao.insertOrderItem(conn, item);
}
conn.commit();
return true;
} catch (Exception e) {
if (conn != null) {
try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); }
}
e.printStackTrace();
return false;
} finally {
DBUtil.close(conn);
}
}
这里需要注意一点,insertOrder之后要拿到自增主键,JDBC的PreparedStatement可以通过RETURN_GENERATED_KEYS拿到,然后用getGeneratedKeys()取出来。如果这一步卡住,说明对JDBC的预编译机制还不够熟,刚好借此补一下。
4.3 订单审核:一个简化版审批流
订单提交之后需要有人审核,这是业务上的必要环节。审核的本质是状态字段的流转,不需要设计复杂的流程引擎。我用一个status字段来表达订单状态:0表示待审核,1表示审核通过,2表示已入库,3表示已驳回。
审核页面用jQuery发起异步请求,例子如下:
javascript复制$.post('/order/audit', {
orderId: 21,
status: 1
}, function(res) {
if (res.code === 200) {
alert('审核通过');
location.reload();
} else {
alert(res.msg);
}
}, 'json');
后端的Servlet拿到请求后,先检查当前订单状态是不是0,只有待审核的订单才能执行审核动作,否则会提示“该订单已审核,请勿重复操作”。这个状态校验很重要,防止重复提交。
如果想在答辩里加点亮点,可以把这个功能描述成“简化版的审批流”:状态字段驱动订单流转,不同的操作对应不同的状态迁移,将来换成多级审批只是增加状态节点的问题。这恰好就是热门话题里“JSP项目前端用js+jquery实现审批流”的思路落地。
4.4 到货入库:整个系统最重要的事务
订单审核通过后,供应商送货到门店,仓管员在系统里点击到货入库。入库动作同时要做三件事:把订单状态改为已入库,往入库流水表插入记录,最后更新商品表的库存。
这三件事也必须在一个事务里完成。可以设想一下,如果更新库存成功、插入流水失败,那库存多了但记录没有,以后查账根本对不上;如果库存更新一半出错、没有回滚,那更严重,订单里A商品加了库存,B商品没加,整个库存数据全部错乱。
这也是我在带项目时反复强调的点:只要涉及多条更新,必然要开事务。写法和前面创建订单类似,核心就四个步骤:关闭自动提交,执行业务SQL,全部成功才commit,任何一个环节异常就rollback。
4.5 防SQL注入:用PreparedStatement而不是拼字符串
在所有数据库操作中,一律用PreparedStatement,不要用Statement再拼SQL字符串。举个例子,商品名称搜索如果写成:
java复制String sql = "SELECT * FROM goods WHERE goods_name LIKE '%" + keyword + "%'";
一旦keyword里带上了' OR '1'='1这样的内容,SQL语句就变成了一个恒真条件,轻则查询出全部数据,重则被删库。而PreparedStatement用占位符?传参,SQL语句的结构和数据是分离的,数据库端会做参数化处理,注入的SQL片段只会被当成普通字符串,不会成为可执行代码。
这是一个技术细节,但它是答辩时老师最爱问的安全问题,一定要熟练表达出来。
5. JSP页面开发中的几个硬核细节
业务代码写通了,页面开发也有几个特别值得注意的细节,这些细节决定了你的系统会不会被一眼看穿是“拼凑的”。
5.1 用EL和JSTL代替JSP脚本片段
很多学生写JSP还是老风格,在HTML里穿插<% ... %>脚本片段。这个写法在小项目里看起来没问题,但一旦页面复杂,脚本片段和HTML混在一起,阅读和修改都非常痛苦。
更关键的是,JSP里写Java代码会让业务逻辑分散在页面中,等于把MVC打破了。如果在JSP中直接写<% conn = DBUtil.getConnection(); %>,那维护成本瞬间上升。我建议页面展示数据统一用EL表达式和JSTL标签:
jsp复制<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<c:forEach items="${page.list}" var="order">
<tr>
<td>${order.orderNo}</td>
<td>${order.supplierName}</td>
<td>${order.totalAmount}</td>
<td>${order.statusDesc}</td>
</tr>
</c:forEach>
JSP页面里只出现标签和表达式,Java代码留在Servlet和Service中,后续维护页面的时候,基本只需要改HTML结构,不会动到业务逻辑。这也是对“在JSP上写了大量Java代码的风险”最好的解决方案。
5.2 分页、模糊搜索和ajax实时查库存
进货订单和商品列表数据会越来越多,必须加分页。分页的SQL是LIMIT ? OFFSET ?,第一个问号是每页数量,第二个问号是跳过的记录数。后端用一个PageBean<T>对象封装当前页数据、总数、总页数,前端只负责渲染。
模糊搜索就是商品名称用LIKE查询,供应商名称也用LIKE,这类查询放在DAO层统一封装。
比较能体现交互水平的是用ajax实时查库存。比如在创建进货单的页面上,选中一个商品后,向后台发一个请求,及时提示“当前库存剩余xx件,建议补货xx件”,不需要刷新页面,体验会好很多。代码也很简单:
javascript复制$.get('/goods/detail', { goodsId: 8 }, function(res) {
$('#stockInfo').text('当前库存:' + res.stock + '件');
}, 'json');
5.3 中文乱码:三处设置一次说清
JSP中文乱码问题是个经典老坑。要一次性搞定,需要检查三处。第一处,JSP页面顶部必须写:
jsp复制<%@ page contentType="text/html;charset=UTF-8" language="java" %>
第二处,请求参数在处理之前设置编码,最好在Filter里统一处理:
java复制request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
第三处,数据库连接URL要加上编码参数:
code复制jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
从Tomcat 8开始,GET请求默认就是UTF-8,所以只要POST请求在Filter里设置一次,基本不会再乱码。如果还乱码,检查数据库表的排序规则是否为utf8mb4。
5.4 文件上传和浏览器FakePath的问题
很多人想在页面上做一个“导入商品Excel”的功能,会遇到一个困惑:用<input type="file">选择文件后,通过JS只能拿到一个假的路径,比如C:\fakepath\goods.xls,拿不到真实的本地路径,在网上搜答案越搜越迷糊。
这是浏览器的安全策略,不是代码写错了。Chrome、Firefox等现代浏览器出于安全考虑,不允许网页读取客户端磁盘的完整路径。正确做法不是去获取路径,而是把整个文件通过表单提交给后端Servlet,由后端把文件流保存到服务器的某个目录,再在系统里记录保存后的相对路径。
Servlet 3.0提供了Part接口,处理起来很方便:
java复制Part filePart = request.getPart("file");
String fileName = filePart.getSubmittedFileName();
String savePath = uploadDir + File.separator + fileName;
filePart.write(savePath);
如果有解析Excel的需求,保存后再用POI库读取文件内容,批量插入到商品表。想给项目加这个功能的同学,可以按照这个思路走,别在获取客户端路径上浪费时间了。
5.5 页面静态资源引用路径问题
JSP页面里引CSS、JS时,如果直接写相对路径,一旦请求地址层级发生变化,资源就会加载失败。最稳妥的办法是先用JSTL定义一个上下文变量:
jsp复制<c:set var="ctx" value="${pageContext.request.contextPath}" />
然后所有静态资源都通过${ctx}拼接:
html复制<link rel="stylesheet" href="${ctx}/static/css/bootstrap.min.css">
<script src="${ctx}/static/js/jquery.min.js"></script>
这样无论从哪个路径访问JSP页面,资源都能正确加载,不会出现页面样式全丢的情况。这也能顺带解决登录后重定向到子页面,CSS样式全部变形的常见问题。
6. 答辩前要准备的事:高频追问和快速加分项
系统做完了,代码也能跑,还剩一道关口:答辩。老师提问基本不会跑出业务和技术的交集,把接下来的几个问题准备好,心里就有底了。
6.1 答辩时老师最常追问的五个问题
-
“你这个系统是什么架构?”回答要点是:B/S结构,MVC分层,JSP负责视图,Servlet负责控制,JavaBean和DAO负责业务和数据库访问,数据库用MySQL,Web服务器用Tomcat。
-
“进货订单为什么要拆成两张表?”回答要点是一对多关系,一个订单包含多个商品,拆表可以避免数据冗余,同时便于统计订单金额和追溯入库明细。可以直接拿购物小票做类比。
-
“入库时怎么保证库存数据和流水一致?”回答要点是数据库事务,commit之前任何一个环节失败都rollback。如果老师追问事务的ACID特性,也能顺着说出原子性和一致性。
-
“怎么防止SQL注入?”回答要点是用PreparedStatement参数化查询,SQL结构不参与拼接。可以现场简单演示一个带
' OR '1'='1的例子。 -
“密码是怎么存储的?”如果设计时只是明文存储,会被扣分。建议用MD5加盐存储,答辩时直接说“我用了加盐哈希,没有存储明文密码”,这是典型的加分回答。
6.2 三个能给毕设加分的扩展点
- 库存预警:在商品列表里把低于
min_stock的商品标红,用一句话就能讲清楚逻辑,但效果直观。 - 进货统计报表:按月份或供应商维度统计进货总额,前端用一个小柱状图展示,推荐用ECharts,半天的成本就可以实现。
- Excel导出:用POI把进货订单明细导出成Excel,实用性很强,演示效果也好。
这几个扩展点工作量大不大?不大,但它们共同传递了一个信号:你做的不只是一个简单的增删改查,而是考虑了实际使用中会用到的统计和分析需求。
6.3 JSP方案到Spring Boot方案的迁移思路
答辩时老师很可能顺口问一句:“如果现在让你用Spring Boot重写,你会怎么做?”这个问题不是要你现场改代码,而是考察你对技术演进的理解。
迁移思路很清晰:Servlet对应Spring MVC里的Controller,DAO对应MyBatis的Mapper,JSP对应Thymeleaf模板,数据库连接池从Druid继续复用。原来用过滤器做登录拦截,Spring Boot里改成拦截器或Spring Security即可。表结构基本不用动,Service层的业务逻辑可以直接搬。能说出这套迁移思路,说明你对系统的理解不是停留在某个具体技术上,而是看到了业务和技术分层之间的关系。
最后说句实在话。超市进货管理系统不是什么高深项目,它赢在业务闭环清晰、数据关系典型,适合完整走一遍需求、设计、开发、测试的过程。只要你把“订单、审核、入库、库存”这条线彻底跑通,把每一张表的关系和每个事务的边界讲清楚,代码是自己一行一行敲的,答辩就没什么可慌的。做完这个系统,你会发现其他管理类题目都变成了同一个套路的不同变体,这正是它作为毕业设计最有价值的地方。
