简介:这份资源是面向高校计算机相关专业学生与Java Web初学者的一套农产品销售管理系统完整项目,基于Java、JSP与Tomcat技术栈开发,采用MySQL数据库与B/S架构,适合用作课程设计、毕业设计或相关项目实战参考。压缩包整体约95.93MB,内含项目全套源码与完整文档,源码均经过测试校正,可保证百分百成功运行,便于读者直接部署与二次开发。系统围绕农产品销售业务展开,涵盖商品管理、订单处理、用户权限等典型模块,能帮助读者理解JSP页面与后端逻辑的协作方式、数据库表结构设计以及Tomcat环境下的项目部署流程。目前已有1440人学习下载,说明该方案在同类项目设计中具有一定参考价值。对于需要快速搭建Web项目、梳理B/S架构开发思路或寻找可运行案例的读者,这份源码加文档的组合能提供从代码实现到设计说明的完整支撑,节省自行摸索的时间。
1. 农产品销售管理系统:从 JSP 页面到 MySQL 落库,一套能跑通的 B/S 全链路
很多同学做毕设或接私活时,一看到「基于 Java+JSP+Tomcat 的农产品销售管理系统」就头大:JSP 到底还要不要学?Tomcat 怎么配?MySQL 表怎么设计才不返工?我当年第一次搭这套东西,光是把一个农产品列表页从数据库读到浏览器上就折腾了一整天,翻车翻在 JDBC 驱动没放进WEB-INF/lib。这套技术栈虽然老,但它是理解 B/S 架构最直白的路径——浏览器发请求,Tomcat 接住,JSP 渲染,MySQL 存数据,四层职责清清楚楚。本文面向要交毕设、要做课程设计、或者想用最低成本搭一个内部销售管理工具的开发者,把环境搭建、表结构设计、JSP 页面开发、Tomcat 部署这条链路一步步拆开,参数怎么设、坑在哪,都写清楚。
2. 环境选型与工程骨架:JDK、Tomcat、MySQL 三件套怎么配不打架
2.1 版本组合为什么比「装最新版」更重要
这套技术栈最大的玄学就是版本兼容。JSP 本质是 Servlet 的视图层,Tomcat 负责把它编译成 Servlet 再执行,而 JDK 版本决定了编译目标字节码。我一般推荐 JDK 8 + Tomcat 8.5 或 9 + MySQL 5.7 或 8.0 这个组合,原因是 JDK 8 对 JSP 的<%@ page %>指令支持最稳,Tomcat 8.5 之后对 Servlet 3.1 规范实现完整,MySQL 8.0 的驱动类名从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,这个细节后面会专门讲。
如果你用 IDEA 2026 本地部署 Tomcat 9 却发现找不到 Tomcat Server 选项,通常是因为社区版 IDEA 不内置应用服务器插件,需要手动在 Settings 的 Plugins 里确认 Application Servers 相关支持已启用,或者改用 Tomcat 的startup.bat独立启动再通过浏览器访问。这个坑很多人踩过,不是配置错了,是工具链本身的功能边界。
MySQL 在 Windows 10 上的安装,推荐用 ZIP 压缩包方式而不是 MSI 安装器,因为 ZIP 方式对my.ini的控制更直接。下载地址认准官方归档页,5.7.44 和 8.0 系列都有。安装过程核心就三步:解压、初始化、注册服务。
# 解压到 D:\tool\mysql-8.0.46-winx64 后,以管理员身份进入 bin 目录 mysqld --initialize-insecure --console # --initialize-insecure 表示 root 用户初始密码为空,方便本地开发 # --console 把初始化日志打到控制台,方便看报错 mysqld --install MySQL80 --defaults-file="D:\tool\mysql-8.0.46-winx64\my.ini" # 注册为 Windows 服务,服务名 MySQL80 net start MySQL80 # 启动服务,如果提示"服务正在启动 ."后卡住,多半是 my.ini 里 basedir 或 datadir 路径写错my.ini里必须显式写basedir和datadir,否则 MySQL 会去默认路径找数据目录导致启动失败。初始化完成后用mysql -u root -p登录,第一次直接回车,然后立刻改密码。
2.2 工程目录结构:一个能直接导入 IDEA 的骨架
农产品销售管理系统的典型模块包括:用户管理、农产品分类、农产品信息、订单管理、销售统计。工程按 MVC 分层,目录结构如下:
nongchanpin/ ├── src/ │ └── com/farm/ │ ├── dao/ # 数据库访问层 │ ├── entity/ # 实体类 │ ├── servlet/ # 控制层 │ └── util/ # DBUtil 工具类 ├── WebContent/ │ ├── WEB-INF/ │ │ ├── lib/ # 放 mysql-connector-java.jar │ │ └── web.xml # 部署描述符 │ ├── css/ │ ├── js/ │ └── *.jsp # 视图层WEB-INF/lib这个目录是血泪经验:JDBC 驱动 jar 包必须放在这里,不能只放在项目的 Libraries 里。因为 Tomcat 运行时只认WEB-INF/lib和 Tomcat 自己的lib目录,IDEA 的 Libraries 配置只在编译期生效,运行期不生效。这就是为什么很多人编译通过但一访问就报ClassNotFoundException: com.mysql.cj.jdbc.Driver。
2.3 数据库连接工具类的写法与参数说明
不要在每个 Servlet 里都写一遍DriverManager.getConnection,抽一个DBUtil:
package com.farm.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.Statement; public class DBUtil { // MySQL 8.0 驱动类名带 cj,5.7 用 com.mysql.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; // useSSL=false 关闭 SSL 警告,serverTimezone 必须设否则时间字段会差 8 小时 private static final String URL = "jdbc:mysql://localhost:3306/farm_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConn() throws Exception { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement st, ResultSet rs) { try { if (rs != null) rs.close(); } catch (Exception e) {} try { if (st != null) st.close(); } catch (Exception e) {} try { if (conn != null) conn.close(); } catch (Exception e) {} } }serverTimezone=Asia/Shanghai这个参数不加,插入的时间字段会比实际时间少 8 小时,因为 MySQL 8.0 默认用 UTC 时区解析。characterEncoding=utf8不加,中文农产品名称会变成问号。这两个参数是必调项,不是可选项。
3. 农产品销售系统的表结构设计与 JSP 页面联动
3.1 五张核心表:从分类到订单的字段取舍
农产品销售系统的数据模型不复杂,但字段设计要考虑实际业务。比如农产品表要不要存库存?要。订单表要不要存收货地址快照?要,因为用户可能改地址,历史订单必须保留当时的地址。
CREATE DATABASE farm_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE farm_db; -- 用户表 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 值,不存明文 real_name VARCHAR(50), phone VARCHAR(20), role TINYINT DEFAULT 0, -- 0 普通用户 1 管理员 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 农产品分类表 CREATE TABLE t_category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0 ); -- 农产品表 CREATE TABLE t_product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, category_id INT, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, unit VARCHAR(10) DEFAULT '斤', -- 农产品按斤/箱卖,单位要留 image VARCHAR(200), description TEXT, status TINYINT DEFAULT 1, -- 1 上架 0 下架 FOREIGN KEY (category_id) REFERENCES t_category(id) ); -- 订单表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT, total_amount DECIMAL(10,2), address VARCHAR(200), -- 地址快照 status TINYINT DEFAULT 0, -- 0 待付款 1 已付款 2 已发货 3 已完成 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES t_user(id) ); -- 订单明细表 CREATE TABLE t_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT, product_id INT, quantity INT, price DECIMAL(10,2), -- 下单时的单价快照 FOREIGN KEY (order_id) REFERENCES t_order(id), FOREIGN KEY (product_id) REFERENCES t_product(id) );utf8mb4而不是utf8,因为农产品描述里可能有 emoji 或特殊符号。DECIMAL(10,2)而不是FLOAT,金额计算不能用浮点数,否则会出现 0.1+0.2=0.30000000000000004 的问题。订单明细里的price是下单时的价格快照,不能关联查产品表当前价格,否则产品调价后历史订单金额就错了。
3.2 JSP 页面读取农产品列表:从 Servlet 到 EL 表达式
农产品列表页是这套系统最核心的页面,链路是:浏览器请求/productList→ Servlet 查数据库 → 把 List 放进 request → 转发到 JSP → JSP 用 JSTL 遍历渲染。
Servlet 端:
package com.farm.servlet; import com.farm.dao.ProductDao; import com.farm.entity.Product; 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 java.io.IOException; import java.util.List; @WebServlet("/productList") public class ProductListServlet extends HttpServlet { @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ProductDao dao = new ProductDao(); // 支持按分类筛选,categoryId 为空则查全部 String cid = req.getParameter("categoryId"); List<Product> list = (cid == null || cid.isEmpty()) ? dao.findAll() : dao.findByCategory(Integer.parseInt(cid)); req.setAttribute("products", list); // 转发而不是重定向,request 域中的数据才能带到 JSP req.getRequestDispatcher("/product_list.jsp").forward(req, resp); } }JSP 端:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %> <html> <head><title>农产品列表</title></head> <body> <table border="1"> <tr><th>名称</th><th>单价</th><th>库存</th><th>单位</th><th>操作</th></tr> <c:forEach items="${products}" var="p"> <tr> <td>${p.name}</td> <td><fmt:formatNumber value="${p.price}" pattern="#,##0.00"/></td> <td>${p.stock}</td> <td>${p.unit}</td> <td><a href="addCart?productId=${p.id}">加入购物车</a></td> </tr> </c:forEach> </table> </body> </html><c:forEach>的items属性对应 Servlet 里setAttribute("products", list)的 key。fmt:formatNumber用来格式化金额,保留两位小数。如果页面显示乱码,检查三处:JSP 的pageEncoding、contentType的 charset、以及数据库连接 URL 里的characterEncoding,三处必须统一为 UTF-8。
3.3 图片上传与坐标定位:农产品图片怎么存怎么显示
农产品系统少不了上传产品图片。JSP 页面用enctype="multipart/form-data"表单提交,Servlet 端用@MultipartConfig注解接收:
@WebServlet("/uploadProduct") @MultipartConfig(maxFileSize = 5 * 1024 * 1024) // 单文件最大 5MB public class UploadServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Part part = req.getPart("image"); String fileName = System.currentTimeMillis() + "_" + part.getSubmittedFileName(); // 存到 webapp 下的 upload 目录,不要存到项目外的绝对路径 String savePath = getServletContext().getRealPath("/upload"); part.write(savePath + File.separator + fileName); // 数据库只存相对路径,不存绝对路径 String dbPath = "upload/" + fileName; // ... 后续把 dbPath 存入 t_product.image } }数据库只存相对路径upload/xxx.jpg,不存D:\tomcat\webapps\...这种绝对路径,否则换一台机器部署图片全挂。JSP 里用<img src="${pageContext.request.contextPath}/${p.image}">显示,contextPath保证项目名变化时路径仍然正确。
如果要做图片坐标定位(比如在农产品图片上标注产地位置),前端用 CSS 的position: relative容器加position: absolute的标记点,坐标存百分比而不是像素值,这样图片缩放时标记点不会跑偏。
4. Tomcat 部署与 JSP 编译期常见问题排查
4.1 部署方式:war 包、exploded、还是直接跑
三种部署方式各有场景。开发阶段用 IDEA 的 exploded 模式,改 JSP 不用重启;测试阶段打 war 包丢进webapps目录;生产环境用 war 包加 Tomcat 的autoDeploy。打 war 包用 Maven 的mvn package或者 IDEA 的 Artifacts 配置。
Tomcat 的server.xml里Connector标签的port默认 8080,如果被占用改成 8081。URIEncoding在 Tomcat 8 以后默认就是 UTF-8,不用手动加。context.xml里可以配数据源,但小系统直接用DBUtil就够了,配 JNDI 数据源反而增加复杂度。
如果遇到「tomcat 后台页面上传 war 被限制 IP」,那是manager应用的context.xml里RemoteAddrValve在起作用,默认只允许本机访问。本地开发不用管,远程部署时改allow属性。
4.2 JSP 第一次访问慢:编译机制与预编译配置
JSP 不是解释执行的,Tomcat 第一次收到 JSP 请求时会把它编译成 Java 文件再编译成 class,所以第一次访问特别慢。这是正常现象,不是性能问题。如果想让生产环境第一次访问就快,可以在web.xml里配jsp-config做预编译,或者在部署后手动访问一遍所有 JSP 页面触发编译。
work目录是 Tomcat 存放编译后 JSP 的地方,清理缓存时删work/Catalina/localhost/项目名即可。有时候改了 JSP 但页面没变化,就是work目录里的旧 class 没被清掉,重启 Tomcat 或手动删work目录能解决。
4.3 中文乱码的三层排查法
中文乱码是这套系统最高频的问题,按三层排查:
第一层,数据库层。建库时用utf8mb4,连接 URL 加characterEncoding=utf8。用SHOW VARIABLES LIKE 'character%'确认character_set_server是utf8mb4。
第二层,请求层。POST 请求在 Servlet 第一行加req.setCharacterEncoding("UTF-8")。GET 请求在 Tomcat 8 以后默认 UTF-8,不用额外处理。如果用的是 Tomcat 7,需要在server.xml的 Connector 里加URIEncoding="UTF-8"。
第三层,响应层。JSP 页面头部contentType="text/html;charset=UTF-8",Servlet 输出前resp.setContentType("text/html;charset=UTF-8")。三层都对齐了,乱码基本不会出现。
5. 避坑与常见问题:那些让我返工三次的细节
5.1 驱动类名不匹配导致连接失败
现象:启动报ClassNotFoundException: com.mysql.jdbc.Driver。 原因:MySQL 8.0 的驱动 jar 里类名是com.mysql.cj.jdbc.Driver,旧教程里写的com.mysql.jdbc.Driver在 8.0 驱动里虽然保留了兼容类但会打警告,某些版本直接移除。 解决:确认mysql-connector-java版本,8.0 以上用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver。同时确认 jar 包在WEB-INF/lib下。
5.2 时间字段差 8 小时
现象:插入数据库的时间比实际时间少 8 小时。 原因:MySQL 8.0 的 JDBC 驱动默认用 UTC 时区解析DATETIME,而服务器在东八区。 解决:连接 URL 加serverTimezone=Asia/Shanghai。或者把 MySQL 全局时区设为+8:00,但改 URL 更简单。
5.3 JSP 页面 EL 表达式不解析
现象:页面上直接显示${p.name}而不是实际值。 原因:web.xml 的web-app标签版本太低(2.3 或以下),EL 表达式默认不启用。 解决:把 web.xml 的 schema 声明改成 Servlet 3.1 对应的版本,或者在该 JSP 的 page 指令里加isELIgnored="false"。
5.4 订单金额用 double 计算出现精度丢失
现象:购物车总价算出99.99999999这种值。 原因:Java 的double和float是浮点数,不能精确表示十进制小数。 解决:金额计算用BigDecimal,数据库字段用DECIMAL。BigDecimal的add、multiply方法做运算,最后setScale(2, RoundingMode.HALF_UP)保留两位。
5.5 Tomcat 启动报端口占用
现象:java.net.BindException: Address already in use: JVM_Bind。 原因:8080 端口被其他程序占用,常见的是另一个 Tomcat 实例或某些开发工具。 解决:netstat -ano | findstr 8080找到占用进程的 PID,taskkill /PID xxx /F杀掉,或者改 Tomcat 的server.xml里 Connector 的 port 为 8081。
6. 销售统计的 SQL 写法与一个可复用的分页技巧
农产品销售系统做到最后,最有价值的模块是销售统计。管理员需要看「每个分类的销售额」「每月订单量」「库存预警」。这些用 SQL 聚合直接出结果,比在 Java 里循环计算高效得多。
按分类统计销售额:
SELECT c.name AS category_name, COUNT(o.id) AS order_count, SUM(oi.quantity * oi.price) AS total_sales FROM t_order_item oi JOIN t_product p ON oi.product_id = p.id JOIN t_category c ON p.category_id = c.id JOIN t_order o ON oi.order_id = o.id WHERE o.status IN (1, 2, 3) -- 排除待付款订单 GROUP BY c.id, c.name ORDER BY total_sales DESC;WHERE在GROUP BY之前过滤,把待付款订单排除掉,统计的才是真实成交额。SUM(oi.quantity * oi.price)用的是订单明细里的价格快照,不是产品表当前价格,保证历史数据准确。
库存预警查询:
SELECT name, stock, unit FROM t_product WHERE stock < 10 AND status = 1 ORDER BY stock ASC;分页是列表页必备的。MySQL 用LIMIT实现:
SELECT * FROM t_product WHERE status = 1 ORDER BY id DESC LIMIT #{offset}, #{pageSize};offset = (pageNum - 1) * pageSize。pageSize 一般设 10 或 20,pageNum 从 1 开始。JSP 页面渲染分页条时,总页数用SELECT COUNT(*)单独查一次,不要用LIMIT查出来的结果数去算。
我自己的习惯是:所有列表查询都带分页,哪怕现在数据只有几条。因为数据量涨起来是很快的,等到几千条再改分页,所有调用点都要动。一开始就写好,后面省事。另外统计类 SQL 不要在 JSP 里直接写,放到 DAO 层,JSP 只负责展示,这样 SQL 出问题的时候好定位。
希望帮到你。
本文还有配套的精品资源,点击获取