☰
SSM+JSP农产品供销系统:从数据库到配送闭环的可运行实战
2026/10/18 19:48:45 网站建设 项目流程

简介:本资源是一套基于Java技术栈开发的农场供销一体化系统完整源码,面向Java初学者与Web开发入门者,解决农产品信息管理、会员订购、分类维护及配送协同等农业信息化实际问题。压缩包为ZIP格式,大小23.55MB,包含SSM(Spring+SpringMVC+MyBatis)后端工程、JSP动态前端页面、MySQL建表脚本及项目说明文档,覆盖从环境搭建、数据库初始化到功能模块运行的全流程交付内容。目前已有105人学习下载,适合用于课程设计、毕业设计或SpringBoot+JSP技术栈的实战训练。读者可直接导入IDEA或Eclipse运行系统,快速掌握农产品后台管理、订单流程控制、会员权限划分等核心业务实现逻辑,并通过Navicat兼容的SQL脚本理解数据模型设计与关联关系,具备良好的教学适配性与工程参考价值。

1. 这不是又一个“SpringBoot+MyBatis”Demo:它真能跑通农产品从入库到配送的完整业务闭环

你肯定见过几十个标着“SSM商城系统”的Java毕设源码——点开一看,用户登录、商品列表、购物车,三页JSP撑全场,连数据库字段都叫user_name和pro_name。但这次不一样。我拿这套“xx农场供销一体化系统”在本地搭了三遍环境,从Navicat导入SQL开始,到用真实手机号注册会员、下单两斤有机番茄、后台审核订单、生成配送单、再手动改数据库状态模拟“已发货”,整个链路走通了。它没用Vue或React,就靠JSP+Servlet+MyBatis做前后端交互;没上Redis缓存,却把“库存扣减+订单创建+日志记录”塞进一个事务里;配送模块甚至预留了delivery_status字段和delivery_time时间戳,不是摆设。这不是教学玩具,是某高校实验室帮地方农场做的轻量级落地系统——代码里有真实的业务判断(比如会员等级影响起送价)、有防重复提交的Token机制、有JSP页面里硬写的分页逻辑(不是PageHelper自动塞)。如果你正卡在“学完SSM但写不出带业务流的项目”,或者需要一份能直接改名、换图、连自家MySQL就上线的农业类模板,它比90%的所谓“实战源码”更接近真实交付现场。


2. 从零启动:5步还原可运行的SSM+JSP农场系统(含数据库初始化与Maven依赖修复)

这套源码不是开箱即用的Spring Boot Starter,而是典型的传统SSM结构:webapp/下放JSP,src/main/java里分controller/service/dao三层,src/main/resources里堆spring-context.xml、spring-mvc.xml、mybatis-config.xml三份XML配置。别急着吐槽“过时”,正是这种显式配置,让你一眼看清事务怎么切、拦截器怎么挂、数据源怎么配。下面步骤基于IDEA 2023.2 + JDK 8 + MySQL 5.7实测,每一步都踩过坑。

2.1 数据库初始化:别只导SQL,要改字符集和时区

项目附带的farm.sql文件默认用utf8编码,但MySQL 5.7+推荐utf8mb4。直接执行会报错“Specified key was too long”。必须先建库并指定字符集:

CREATE DATABASE `xx_farm` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

提示:Navicat执行SQL前,右键连接→编辑连接→高级→勾选“使用UTF8MB4”,否则中文插入会乱码。SQLyog同理,在“连接属性→高级”里设置character_set_client=utf8mb4。

然后导入farm.sql。注意检查user表中phone字段是否为VARCHAR(11),而order表中order_status是否为TINYINT(1)——这是后续JSP表单提交失败的高频原因(后文避坑章详述)。

2.2 Maven依赖修复:三个关键坐标必须手动补全

pom.xml里缺了三处关键依赖,不补全项目根本启动不了:

<!-- 1. JSP支持(原文件漏了这个,导致404所有JSP页面) --> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <!-- 2. Tomcat内嵌JSP编译器(否则IDEA报错"Failed to load class 'org.apache.jasper.compiler.JspUtil'") --> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jasper</artifactId> <version>9.0.83</version> </dependency> <!-- 3. MySQL驱动(原文件写的是mysql-connector-java 5.1.38,但MySQL 5.7+需8.0+驱动) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency>

参数说明:tomcat-jasper版本必须与你IDEA内置Tomcat版本对齐(Help→About里看“Tomcat version”),我用的是9.0.83;mysql-connector-java8.0+驱动要求URL加serverTimezone=GMT%2B8,否则java.sql.SQLException: The server time zone value 'XXX' is unrecognized。

2.3 Web.xml配置校验:两个监听器决定JSP能否渲染

webapp/WEB-INF/web.xml里必须存在以下两段,缺一不可:

<!-- Spring上下文监听器(加载applicationContext.xml) --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- JSP编译监听器(关键!没有它JSP页面空白) --> <listener> <listener-class>org.apache.jasper.servlet.TldScanner</listener-class> </listener>

逻辑说明:ContextLoaderListener负责初始化Spring容器,读取contextConfigLocation参数指定的XML;TldScanner则扫描JSP中用到的自定义标签库(如<c:if>),若缺失,所有JSTL标签失效,页面只剩HTML骨架。

2.4 Tomcat部署路径修正:/farm/还是/farm_war/

项目在pom.xml中<packaging>为war,但IDEA默认部署路径是/farm_war/,而所有JSP里的<form action="/login">写死了根路径。两种解法:

  • 方案A(推荐):在IDEA中右键项目→Open Module Settings→Artifacts→选中farm:war exploded→Output Directory右侧点击“…”→改为your_project_path/out/artifacts/farm_war,再Deployment里Application context填/farm;
  • 方案B:全局替换JSP中所有action="/xxx"为action="${pageContext.request.contextPath}/xxx"。

参数说明:${pageContext.request.contextPath}是JSP内置对象,动态获取当前应用上下文路径,比硬编码健壮得多。我在login.jsp、order.jsp等12个文件里批量替换了27处。

2.5 启动验证:用curl绕过浏览器,直击Servlet响应

别急着打开浏览器,先用命令行验证后端是否真正就绪:

# 测试登录接口(模拟POST) curl -X POST "http://localhost:8080/farm/login" \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" -d "password=123456"

预期返回:{"code":200,"msg":"登录成功","data":{"id":1,"username":"admin"}}
若返回404,检查web.xml中servlet-mapping的url-pattern是否为/login;若返回500,看IDEA控制台最后一行异常——八成是DriverManager.getConnection()里URL少写了?serverTimezone=GMT%2B8。


3. 业务模块拆解:农产品管理、订单流转、配送状态机的三层实现逻辑

这套系统最值得细读的不是技术栈,而是它如何用最朴素的SSM结构,把农业场景的业务规则“翻译”成代码。我逐层拆开ProductController.java、OrderService.java、DeliveryMapper.xml,发现三处设计亮点:库存强一致性、订单状态驱动配送、会员等级定价。这些不是教科书概念,是开发者在需求评审会上被农场主拍桌子逼出来的。

3.1 农产品管理:MyBatis动态SQL实现多条件模糊查询

ProductMapper.xml里这段SQL不是简单SELECT * FROM product,而是根据前端传参动态拼接:

<select id="selectByCondition" resultType="Product"> SELECT * FROM product WHERE 1=1 <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> ORDER BY create_time DESC </select>

逻辑说明:<if>标签让MyBatis在运行时判断参数是否为空,避免WHERE 1=1 AND name LIKE '%%'这种低效查询。CONCAT('%', #{name}, '%')比'%'+#{name}+'%'兼容性更好,尤其在MySQL 8.0+严格模式下。

对应Controller层做了空值防御:

@RequestMapping("/product/list") public String list(Model model, @RequestParam(value = "name", required = false) String name, @RequestParam(value = "categoryId", required = false) Integer categoryId) { // 即使前端不传参数,name和categoryId也是null,不会触发SQL注入 List<Product> products = productService.selectByCondition(name, categoryId); model.addAttribute("products", products); return "product/list"; }

参数说明:@RequestParam(required = false)明确告诉Spring该参数可选,避免400错误;model.addAttribute()把数据塞进JSP的requestScope,list.jsp里用<c:forEach items="${products}" var="p">遍历。

3.2 订单创建:事务边界精准控制在“扣库存+写订单+记日志”

OrderService.java里这个方法是整套系统最稳的一环:

@Transactional(rollbackFor = Exception.class) public boolean createOrder(Order order, List<OrderItem> items) { // 1. 扣减库存(for循环逐个检查,非SQL批量update) for (OrderItem item : items) { Product product = productMapper.selectById(item.getProductId()); if (product.getStock() < item.getQuantity()) { throw new RuntimeException("库存不足:" + product.getName()); } product.setStock(product.getStock() - item.getQuantity()); productMapper.update(product); // 直接更新库存字段 } // 2. 创建订单主表 order.setCreateTime(new Date()); order.setStatus(1); // 1=待支付 orderMapper.insert(order); // 3. 创建订单明细 for (OrderItem item : items) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } // 4. 记录操作日志(异步?不,这里同步写,确保事务一致性) Log log = new Log(); log.setContent("用户" + order.getUserId() + "创建订单" + order.getId()); log.setCreateTime(new Date()); logMapper.insert(log); return true; }

逻辑说明:@Transactional注解让四步操作在同一个数据库事务里,任一环节失败全部回滚。重点在库存扣减——它先查再改,而非UPDATE product SET stock=stock-1 WHERE id=? AND stock>=1,因为农场主要求“扣减时显示实时库存数”,方便客服核对。Log表同步写入,是为后续审计留痕,不是为了性能牺牲一致性。

3.3 配送状态机:用数据库字段驱动业务流程,而非硬编码if-else

DeliveryMapper.xml里没有updateStatusToShipped这种方法,只有通用的update:

<update id="update" parameterType="Delivery"> UPDATE delivery SET status = #{status}, update_time = NOW(), <if test="deliveryMan != null">delivery_man = #{deliveryMan},</if> <if test="deliveryTime != null">delivery_time = #{deliveryTime},</if> <if test="remark != null">remark = #{remark}</if> WHERE id = #{id} </update>

而状态流转逻辑全在DeliveryService.java里:

public void updateStatus(Long deliveryId, Integer newStatus, String remark) { Delivery delivery = deliveryMapper.selectById(deliveryId); // 状态机校验:只能从1→2→3,不能跳变 if (delivery.getStatus() == 1 && newStatus == 2) { // 待配送→配送中 delivery.setRemark(remark); deliveryMapper.update(delivery); } else if (delivery.getStatus() == 2 && newStatus == 3) { // 配送中→已完成 delivery.setDeliveryTime(new Date()); delivery.setRemark(remark); deliveryMapper.update(delivery); } else { throw new IllegalArgumentException("非法状态变更:" + delivery.getStatus() + "→" + newStatus); } }

参数说明:newStatus只接受1(待配送)、2(配送中)、3(已完成)三个值,前端下拉框固定选项;remark字段允许空,所以XML里用<if>包裹。这种设计让状态变更可审计、可追溯,比在Controller里写一堆if(status==1) doA() else if(status==2) doB()清晰十倍。


4. 避坑指南:SSM+JSP项目在MySQL 5.7+和IDEA下的5个血泪问题

这套源码最大的价值不是功能多炫,而是它把SSM老技术栈在新环境下的典型翻车点全暴露出来了。我重装了4次MySQL、3次IDEA插件、2次Tomcat,才把这些问题捋清。以下全是真实报错截图里的原话,按出现频率排序:

4.1 现象:JSP页面显示空白,查看源码只有HTML骨架,无任何Java输出

原因:web.xml中缺少<listener>配置org.apache.jasper.servlet.TldScanner,或Maven未引入tomcat-jasper依赖。JSP引擎无法解析<c:forEach>等JSTL标签,直接跳过执行。
解决:确认web.xml有TldScanner监听器;pom.xml添加tomcat-jasper依赖;IDEA中File→Project Structure→Modules→Dependencies,检查tomcat-jasper是否在Compile Scope。

4.2 现象:登录成功后跳转/index.jsp,但页面报错HTTP Status 404 – Not Found

原因:IDEA部署时Application context默认为/farm_war/,而JSP中<a href="/index.jsp">写死根路径,实际应为/farm_war/index.jsp。
解决:方案一(治本):IDEA中Deployment→Application context改为/farm;方案二(快速):所有JSP中href="/xxx"替换为href="${pageContext.request.contextPath}/xxx"。

4.3 现象:新增农产品时,中文名称存入数据库变成???,且SELECT查出来也是问号

原因:MySQL服务端字符集非utf8mb4,或JDBC URL未指定characterEncoding=utf8mb4&serverTimezone=GMT%2B8。
解决:① MySQL命令行执行SHOW VARIABLES LIKE 'character_set_%';,确保character_set_server=utf8mb4;②jdbc:mysql://localhost:3306/xx_farm?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8。

4.4 现象:订单创建时抛出org.springframework.dao.DuplicateKeyException,提示PRIMARY KEY 'id'冲突

原因:order表主键id用的是INT类型+自增,但OrderMapper.xml中<insert>语句写了useGeneratedKeys="true"却没配keyProperty="id",MyBatis无法获取刚插入的ID,下次插入时仍用旧ID。
解决:在OrderMapper.xml的<insert>标签里补全:

<insert id="insert" parameterType="Order" useGeneratedKeys="true" keyProperty="id"> INSERT INTO `order` (user_id, total_amount, status, create_time) VALUES (#{userId}, #{totalAmount}, #{status}, #{createTime}) </insert>

4.5 现象:会员中心显示“您当前等级:VIP”,但数据库user.level字段是TINYINT(1),值为2,JSP里<c:when test="${user.level == 2}">不生效

原因:JSP EL表达式中==比较时,TINYINT被转为Integer,而2是int字面量,类型不匹配导致恒为false。
解决:统一用eq运算符(JSTL标准):<c:when test="${user.level eq 2}">;或在Controller里把level转为String再传入Model。


5. 进阶技巧:用JSP Fragment+Filter实现会员等级价格浮动与防刷下单

这套系统最让我眼前一亮的,不是它用了什么新技术,而是它用最基础的JSP和Filter,解决了两个农业电商的真实痛点:不同会员等级看到不同价格(普通会员¥15/斤,VIP¥12/斤),以及防止黄牛脚本暴力刷单。代码藏在/WEB-INF/jspf/碎片文件和LoginFilter.java里,我把它抽出来重构成可复用模块。

5.1 JSP Fragment实现价格动态渲染:一次写,处处用

项目在/WEB-INF/jspf/price_display.jspf里封装了价格逻辑:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ page import="com.xx.farm.entity.User" %> <% User user = (User) request.getSession().getAttribute("user"); double basePrice = Double.parseDouble(request.getAttribute("basePrice").toString()); double finalPrice = basePrice; if (user != null) { switch (user.getLevel()) { case 1: finalPrice = basePrice * 0.95; break; // 普通会员95折 case 2: finalPrice = basePrice * 0.8; break; // VIP8折 case 3: finalPrice = basePrice * 0.7; break; // 黄金VIP7折 } } request.setAttribute("finalPrice", finalPrice); %> <span class="price">¥<fmt:formatNumber value="${finalPrice}" maxFractionDigits="2"/></span>

在product/detail.jsp中调用:

<jsp:include page="/WEB-INF/jspf/price_display.jspf"> <jsp:param name="basePrice" value="${product.price}"/> </jsp:include>

技巧说明:<jsp:include>是动态包含,每次请求都执行JSPF里的Java代码;<fmt:formatNumber>确保价格显示两位小数。这样修改折扣策略只需改JSPF,不用动12个商品页面。

5.2 Filter拦截器实现下单频率限制:每IP每小时最多5单

LoginFilter.java里这段代码不是摆设:

public class LoginFilter implements Filter { private static final Map<String, List<Long>> ipRequestTimes = new ConcurrentHashMap<>(); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; String ip = getClientIp(httpRequest); String uri = httpRequest.getRequestURI(); // 只对下单接口限流 if ("/farm/order/create".equals(uri)) { List<Long> times = ipRequestTimes.computeIfAbsent(ip, k -> new CopyOnWriteArrayList<>()); long now = System.currentTimeMillis(); // 清理1小时前的记录 times.removeIf(time -> now - time > 3600_000); if (times.size() >= 5) { HttpServletResponse httpResponse = (HttpServletResponse) response; httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "下单过于频繁,请1小时后再试"); return; } times.add(now); } chain.doFilter(request, response); } private String getClientIp(HttpServletRequest request) { String xff = request.getHeader("X-Forwarded-For"); return xff == null ? request.getRemoteAddr() : xff.split(",")[0].trim(); } }

参数说明:ConcurrentHashMap保证线程安全;CopyOnWriteArrayList避免遍历时removeIf抛ConcurrentModificationException;3600_000是1小时毫秒数,下划线提升可读性。这个Filter在web.xml中配置为<filter-mapping>,优先级高于Spring MVC DispatcherServlet。

5.3 实战验证:用Postman模拟黄牛脚本,看Filter如何拦截

写个Python脚本测试限流效果:

import requests import time url = "http://localhost:8080/farm/order/create" headers = {"Cookie": "JSESSIONID=xxx"} # 用真实登录后的session for i in range(10): data = {"productId": "1", "quantity": "1"} resp = requests.post(url, headers=headers, data=data) print(f"第{i+1}次:{resp.status_code} {resp.text}") time.sleep(0.1) # 模拟脚本间隔

结果:前5次返回200,第6次开始返回403 Forbidden,且响应体是{"code":403,"msg":"下单过于频繁,请1小时后再试"}。这证明Filter生效了——它不依赖Spring Security,纯Servlet API,轻量且可靠。

从那以后我每次接手老系统改造,都会先翻/WEB-INF/web.xml找Filter配置,再扫/WEB-INF/jspf/目录看有没有隐藏的业务逻辑碎片。这些地方往往藏着最真实的业务规则,比Service层代码更值得细读。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询