SSM+JSP农场供销系统源码解析:从工程结构到部署排错

SSM+JSP农场供销系统源码解析:从工程结构到部署排错 简介一套基于Java的SSM与JSP农场供销一体化系统源码面向需要完成课程设计、毕业设计或希望系统学习SSM整合开发的Java学习者。系统围绕农场供销真实需求设计了管理员后台与会员前台两大端覆盖农产品信息管理、分类管理、在线订购、会员管理、配送等完整业务流程并实现了前后台权限分离能够帮助读者直观理解一个完整Web项目从需求分析到编码实现的过程。资源以ZIP压缩包形式提供整体约23.55MB文件总数暂未单独展示核心内容为项目源码与说明文档源码采用Spring、SpringMVC、MyBatis、JSP、MySQL、Maven等技术搭建分层清晰便于对照学习控制层、业务层、数据访问层的协作方式也适合分析前后端数据交互与数据库表设计。已有104人学习下载借助该包可节省环境搭建与业务梳理时间适合作为农业信息化方向的课程作业、毕业设计或二次开发的基础参考。1. 一套 SSMJSP 农场供销系统源码值得读的从来不是能跑起来一个 xx 农场供销一体化系统的 SSMJSP 源码包你最先遇到的问题往往不是“能不能跑”而是“从哪开始读”。这类项目在毕业设计和课程设计中出镜率极高技术栈也相当固定Spring 管对象、SpringMVC 管请求路由、MyBatis 管数据库访问、JSP 负责页面渲染再配合采购入库、销售出库、库存台账、供应商与客户管理、工作台统计这几个供销核心模块。如果你正打算用 SSM 做毕设或者刚接手一个老 Java Web 项目需要快速摸清代码脉络甚至只是在刷 java 八股文时想把 IoC、AOP、Mapper 代理这些概念落到真实代码里这套源码都是一个很合适的解剖样本。下文不按文档顺序复述功能而是按“工程结构 → 三层实现 → JSP 联动 → 部署排错”这条线把源码里值得挑出来看的细节讲清楚。2. SSMJSP 农场供销系统的工程结构与关键配置拿到源码第一件事不是启动 Tomcat而是先把目录结构和配置文件理一遍。SSM 项目虽然不像 Spring Boot 那样开箱即用但它的配置链路非常透明每一个加载环节都对应一个 XML 文件把这几个文件的关系搞清楚后面改任何功能都不会慌。2.1 从 Maven 骨架开始的目录解读典型 SSM 项目的目录分为四块我一般会按 src/main/java → src/main/resources → src/main/webapp → 数据库脚本这个顺序去看。farm-supply-chain/ ├── pom.xml ├── src/main/java/com/farm/ │ ├── controller/ # SpringMVC 控制器供销订单、库存、报表入口 │ ├── service/ # 业务接口和实现事务注解基本都在这里 │ ├── mapper/ # MyBatis Mapper 接口只有方法声明 │ ├── entity/ # 数据库表对应的实体类 │ └── common/ # 分页、统一返回结果、工具类 ├── src/main/resources/ │ ├── applicationContext.xml # Spring 根容器管 service 和 mapper │ ├── spring-mvc.xml # SpringMVC 子容器管 controller 和视图 │ ├── mybatis-config.xml # MyBatis 全局配置 │ ├── jdbc.properties # 数据库连接信息 │ └── log4j.properties └── src/main/webapp/ ├── WEB-INF/web.xml # 整个应用的启动入口 ├── WEB-INF/jsp/ # JSP 页面受 WEB-INF 保护不能直接 URL 访问 └── static/ # CSS、JS、图片entity包里的类与数据库表一一对应字段命名通常采用驼峰比如数据库的supplier_name对应实体里的supplierName这是后面 MyBatis 开启驼峰映射的前提。common包里一般会有Result或ResponseData类用来统一 Controller 的返回结构前端 Ajax 判断code字段就知道请求是否成功。2.2 pom.xml 里决定项目命运的依赖清单源码里的 pom.xml 是整个项目能不能编译的底层保障。SSM 不是单框架依赖之间版本不匹配就会启动就报错。以下是这套供销系统最常见的依赖组合注意看表格里的版本说明。依赖版本段在本项目里的作用常见坑spring-webmvc / spring-jdbc5.2.x 或 5.3.xMVC 框架与事务管理与 javax.servlet-api 版本冲突会报 NoClassDefFoundErrormybatis3.5.x数据库 ORM 框架3.5.0 以下对 JDK8 支持不完整mybatis-spring2.0.x把 MyBatis 的 SqlSessionFactory 交给 Spring 管理与 Spring5 配合需要 2.0mysql-connector-java8.0.xJDBC 驱动8.x 驱动类名是 com.mysql.cj.jdbc.Driverjstl1.2JSP 里用 c:forEach、c:if 标签缺这个 JSP 页面会出现无法解析 taglib 报错javax.servlet-api3.1.0 或 4.0.1编译期需要运行期由 Tomcat 提供scope 必须是 provided否则和 Tomcat 自带类冲突jackson-databind2.9.x 或 2.13.xController 返回 JSON 时做序列化与 Spring5 默认的 Jackson 版本重复会报 mapper 冲突poi-ooxml4.1.x把供销订单导出为 Excel依赖传递链路长排除冲突需要小心有一个高频错误值得在这里提示servlet-api 的 scope 写成默认的 compile部署到 Tomcat 后启动时会报java.lang.NoClassDefFoundError: javax/servlet/ServletOutputStream。这就是热词“java 环境变量配置”之外的另一个环境类隐患不是 JDK 的问题而是依赖作用域错了。2.3 三个 XML 配置文件在服务启动时发生了什么web.xml 是整个 SSM 项目的装配总开关。常见做法是配置两类内容一个ContextLoaderListener用来加载applicationContext.xml另一个DispatcherServlet用来加载spring-mvc.xml。加载顺序在 web.xml 里是固定的listener 先于 servlet 执行。listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping这里的关键逻辑是 Spring 的父子容器机制。applicationContext.xml负责 service、mapper 这些业务层对象spring-mvc.xml只扫描 controller。如果把 service 的扫描也放到 spring-mvc.xml事务注解会失效因为 Spring AOP 默认只能拦截 Spring 容器内对象的调用细节问题在经验不足的项目里非常容易踩。applicationContext.xml里做三件事开启注解扫描排除 controller、读取jdbc.properties、配置SqlSessionFactoryBean的 mapperLocations 指向classpath:mapper/*.xml。而mybatis-config.xml里通常只有驼峰映射和日志输出这两项配置作用域被设计得非常窄这一点在 java 面试八股文里也常被问到为什么 MyBatis 的全局配置和 Spring 配置要分开因为职责不同前者管 SQL 层面的行为后者管对象生命周期。3. SSM 三层架构在供销订单场景里的代码实现工程结构看明白后接下来要把三层代码串成一条线。这里以“农资采购入库”和“销售出库扣减库存”这两个供销场景为例从表结构、Mapper XML、Service 事务到 Controller 返回值把一条完整链路走一遍。这也是源码里最值得反复看的部分。3.1 供销系统的核心表结构与实体映射农场供销系统的核心表通常落在采购和销售两张主表上配一张商品表和一张库存表。下面是最简化的设计实际源码里字段会更多但骨架不变。CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 商品名称, spec VARCHAR(64) COMMENT 规格如50kg/袋, unit VARCHAR(16) COMMENT 单位, stock_quantity INT DEFAULT 0 COMMENT 当前库存, safety_stock INT DEFAULT 10 COMMENT 库存预警阈值, price DECIMAL(10,2) COMMENT 参考价, version INT DEFAULT 0 COMMENT 乐观锁版本号 ); CREATE TABLE purchase_order ( id INT PRIMARY KEY AUTO_INCREMENT, supplier_id INT NOT NULL, total_amount DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT 0草稿 1已入库 2作废, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, total_amount DECIMAL(10,2), status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );实体类Product对应product表注意stock_quantity映射到stockQuantityDECIMAL映射到BigDecimal。这里的version字段很重要后面讲并发扣库存会用到。项目里往往还会给每个表配上 create_time、update_time 这类通用审计字段MyBatis 可以在插入时通过set标签填默认值也可以交给数据库侧处理。3.2 Mapper 接口与 XML SQL 的关键写法MyBatis 的一个核心考点是Mapper 接口没有实现类Spring 启动时通过 JDK 动态代理生成代理对象。你调用ProductMapper.selectById(id)本质是代理对象去解析对应的 XML namespace 和方法 id执行 SQL 并做结果映射。mapper namespacecom.farm.mapper.PurchaseOrderMapper resultMap idPurchaseOrderMap typecom.farm.entity.PurchaseOrder id propertyid columnorder_id/ result propertysupplierName columnsupplier_name/ collection propertyitems ofTypecom.farm.entity.PurchaseOrderItem id propertyid columnitem_id/ result propertyproductName columnproduct_name/ result propertyquantity columnquantity/ /collection /resultMap select idselectOrderDetail resultMapPurchaseOrderMap SELECT o.id AS order_id, s.name AS supplier_name, oi.id AS item_id, p.name AS product_name, oi.quantity FROM purchase_order o LEFT JOIN supplier s ON o.supplier_id s.id LEFT JOIN purchase_order_item oi ON o.id oi.order_id LEFT JOIN product p ON oi.product_id p.id WHERE o.id #{id} /select /mapper这段 XML 里有三个值得注意的参数行为。一是#{id}是预编译占位符最终由 JDBC 的 PreparedStatement 设值能防 SQL 注入二是collection用来完成一对多关联这里ofType指向集合元素类型不能写成javaType三是LEFT JOIN的结果集会按 order_id 分组MyBatis 自动把多行 item 组装成一个 List。如果供应商表不存在LEFT JOIN 也能保证主订单数据不丢这比 INNER JOIN 更符合订单详情的查询需求。3.3 Service 层的事务边界与库存扣减Service 层是供销系统里业务逻辑最集中的地方核心是“入库加库存、出库减库存”这两组操作。减库存这个动作如果写成先 SELECT 再 UPDATE在并发下极易出问题常见的正确姿势是把条件写进 UPDATE 语句本身让数据库行锁来保证原子性。Service public class SaleOrderServiceImpl implements SaleOrderService { Autowired private SaleOrderMapper saleOrderMapper; Autowired private ProductMapper productMapper; Transactional(rollbackFor Exception.class) Override public void createSaleOrder(SaleOrder order, ListSaleOrderItem items) { saleOrderMapper.insert(order); for (SaleOrderItem item : items) { int affected productMapper.deductStock(item.getProductId(), item.getQuantity()); if (affected 0) { throw new RuntimeException(商品ID item.getProductId() 库存不足); } } // 记录流水、更新客户应收等其他业务动作 } }update iddeductStock UPDATE product SET stock_quantity stock_quantity - #{quantity}, version version 1 WHERE id #{productId} AND stock_quantity #{quantity} /update这段逻辑的关键在于affected 0的判断。MySQL 的 UPDATE 语句如果匹配了 WHERE 条件但值没变化返回的影响行数可能是 0这里的stock_quantity #{quantity}起着双重保护作用一方面避免库存减成负数另一方面让并发请求只有一个能成功。Transactional(rollbackFor Exception.class)保证了订单插入失败或任一商品库存不足时之前已执行的 SQL 全部回滚。如果这里的 rollbackFor 换成RuntimeException.class或干脆不写像 IOException、SQLException 这类受检异常就不会触发回滚这是 SSM 项目里一个非常经典的隐性 bug。3.4 Controller 层如何把数据变成 JSON 或 JSP 页面Controller 在 SSM 项目里承担路由分发职责同时要遵守一个约定返回字符串就是视图名返回对象加ResponseBody就是 JSON。供销系统里的列表接口和报表接口通常走 JSON页面跳转走视图名。Controller RequestMapping(/sale) public class SaleOrderController { Autowired private SaleOrderService saleOrderService; GetMapping(/create) public String createPage() { return sale/create; } PostMapping(/save) ResponseBody public Result save(RequestBody SaleOrder order) { try { saleOrderService.createSaleOrder(order, order.getItems()); return Result.success(下单成功); } catch (Exception e) { return Result.error(e.getMessage()); } } }这里RequestBody把前端传来的 JSON 字符串反序列化成 SaleOrder 对象依赖的是 Jackson 的 ObjectMapper所以 pom.xml 里必须有 jackson-databind。PostMapping 和 GetMappin 区分了请求方法避免表单重复提交Result是对外统一返回结构里面一般有 code、message、data 三个字段前端 Ajax 通过 code 判断业务是否成功。4. JSP 视图与供销数据联动——从订单渲染到 Excel 导出JSP 是这套系统中最“老”也最容易出问题的一层。它本质是 Servlet第一次被访问时由 JSP 引擎翻译成 Java 类再编译成 .class 执行。理解了这一点很多 JSP 相关的怪现象就都能解释比如热词里高频出现的“jsp 改了不生效”和“jsp 编译 class 文件保存在哪里”。4.1 用 JSTL 和 EL 把订单列表渲染成表格采购订单列表页面是供销系统里边看边学的典型例子。Controller 把订单列表放进 ModelJSP 用 EL 和 JSTL 标签把数据循环输出到表格里。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % table classtable table-bordered thead tr th订单号/th th供应商/th th金额/th th状态/th th创建时间/th /tr /thead tbody c:forEach items${orderList} varorder varStatusst tr td${order.id}/td td${order.supplierName}/td tdfmt:formatNumber value${order.totalAmount} pattern#,##0.00//td td c:choose c:when test${order.status 0}草稿/c:when c:when test${order.status 1}已入库/c:when c:otherwise已作废/c:otherwise /c:choose /td tdfmt:formatDate value${order.createTime} patternyyyy-MM-dd HH:mm//td /tr /c:forEach /tbody /tablec:forEach的items对应 Controller 里放入 Model 的集合名var是循环变量varStatus提供索引等循环状态。fmt:formatNumber和fmt:formatDate负责数字和日期的格式化这意味着不能在前端硬拼字符串方便后续做导出或其他视图。如果 JSP 页面在WEB-INF/jsp目录下浏览器无法直接访问必须通过 Controller 里 return sale/list 这种方式转发进来这也是 SSM 项目里页面跳转的固定姿势。4.2 jsp 改了不生效的真正原因与排查路径开发模式里常遇到一种情况改了 JSP 源码刷新浏览器还是旧内容。这就要回到 JSP 的编译机制。Tomcat 会为每个 JSP 生成一个 _jsp.java 和 _jsp.class 文件存放位置在 Tomcat 的 work 目录下路径类似work/Catalina/localhost/项目名/org/apache/jsp/。如果这个编译产物没有失效Tomcat 会直接执行 class 文件而不会重新编译 JSP 源码。常见做法是分两步排查。第一步看 Tomcat 的 work 目录里对应路径存在不存在存在就删掉整个 org 目录重启这个操作称为“清缓存”第二步检查 IDEA 里是否开了自动编译Tomcat 集成插件需要在“Update resources”模式下改动才会被热替换。还有一类是浏览器缓存用 CtrlF5 强制刷新可以排除。模板里引用的静态 JS/CSS 文件同理每次改完版本号即可。4.3 用 POI 把供销订单导出为 Excel另一个 JSP 相关的高频需求是“jsp 实现数据导出为 excel”。后端生成文件流返回给浏览器JSP 不需要渲染任何内容只要设置响应头让浏览器下载即可。GetMapping(/export) public void exportSaleOrders(HttpServletResponse response) throws IOException { ListSaleOrder orderList saleOrderService.listAllOrders(); XSSFWorkbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(销售订单); Row header sheet.createRow(0); String[] columns {订单号, 客户, 金额, 状态, 下单时间}; for (int i 0; i columns.length; i) { header.createCell(i).setCellValue(columns[i]); } int rowNum 1; for (SaleOrder order : orderList) { Row row sheet.createRow(rowNum); row.createCell(0).setCellValue(order.getId()); row.createCell(1).setCellValue(order.getCustomerName()); row.createCell(2).setCellValue(order.getTotalAmount().doubleValue()); row.createCell(3).setCellValue(order.getStatusDesc()); row.createCell(4).setCellValue(order.getCreateTime() null ? : order.getCreateTime().toString()); } String fileName 销售订单_ System.currentTimeMillis() .xlsx; response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filename URLEncoder.encode(fileName, UTF-8)); workbook.write(response.getOutputStream()); workbook.close(); }导出接口里有两个参数细节是实践中比较重要的。第一XSSFWorkbook对应 .xlsx 格式适合数据量几万条以内的场景如果数据量更大就要改用 SXSSFWorkbook 流式写入否则内存会被撑爆。第二Content-Disposition里的文件名必须用URLEncoder.encode处理否则中文名在部分浏览器里会出现乱码setContentType也不能写成 text/html否则浏览器会直接打开而不是下载。4.4 Ajax 提交与 JSP 页面的局部联动供销系统的下单和入库操作成熟一点的项目不会整页刷新而是用 jQuery 的 Ajax 调 Controller 接口成功后刷新表格区域。$.ajax({ url: ctxPath /sale/save, type: POST, contentType: application/json, data: JSON.stringify(saleOrderVo), dataType: json, success: function (res) { if (res.code 200) { alert(res.message); window.location.href ctxPath /sale/list; } else { alert(res.message); } }, error: function (xhr, status, error) { alert(请求失败: error); } });这里的ctxPath是页面里用${pageContext.request.contextPath}赋值的上下文路径变量避免硬编码http://localhost:8080/项目名。需要说明的是Controller 的/sale/save路径前不要带项目名项目名是 Tomcat 部署时决定的上下文路径代码里只要写相对路径结合 ctxPath 拼完整地址即可。这个做法在 SSM 项目迁移部署时能省掉大量因为路径写死而弹出的 404。5. 从源码到可演示项目——数据库脚本与二次开发技巧源码包里通常附带一份 sql 文件但不少人导入后一启动就报表不存在、字段对不上。这里给出一个最简可用的建库脚本骨架再讲几个部署排错和二次开发的实战技巧。5.1 建库脚本与初始数据的一步到位写法CREATE DATABASE IF NOT EXISTS farm_db DEFAULT CHARACTER SET utf8mb4; USE farm_db; CREATE TABLE IF NOT EXISTS product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL, spec VARCHAR(64), unit VARCHAR(16), stock_quantity INT DEFAULT 0, safety_stock INT DEFAULT 10, price DECIMAL(10,2), version INT DEFAULT 0 ); INSERT INTO product (name, spec, unit, stock_quantity, safety_stock, price) VALUES (有机化肥, 50kg/袋, 袋, 200, 30, 120.00), (玉米种子, 2kg/袋, 袋, 80, 20, 45.50), (除草剂, 500ml/瓶, 瓶, 150, 50, 35.00);数据库连接串里务必加上characterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiMySQL 8.x 不加 timezone 参数会报时区异常。导入脚本时如果报中文乱码检查 SQL 文件本身的编码是否 utf8mb4。5.2 部署排错JDK、Tomcat 与 Maven 的三个高频坑源码能编译不代表能启动部署 SSM 老项目时我一般按下面三条顺序排查。现象检查点处理方式启动报 UnsupportedClassVersionErrorJDK 版本与编译版本不一致把 pom.xml 里 maven.compiler.source/target 调到项目实际 JDKTomcat 启动报 jar 包冲突servlet-api 或 jsp-api 被打进 war 包pom.xml 里这两个依赖 scope 改为 provided数据库中文乱码连接串与数据库字符集不一致统一为 utf8mb4并在连接串加 characterEncodingutf85.3 工作台统计页的经典 SQL 写法供销系统的工作台首页一般会展示库存预警、销售趋势和 Top 商品排行榜这三条 SQL 可以打包复用。-- 近7天每日销售金额趋势 SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS sale_date, SUM(total_amount) AS amount FROM sale_order WHERE create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY sale_date; -- 库存低于安全库存的商品 SELECT name, stock_quantity, safety_stock FROM product WHERE stock_quantity safety_stock ORDER BY stock_quantity ASC; -- 销售 Top 5 商品 SELECT p.name, SUM(oi.quantity) AS total_qty FROM sale_order_item oi JOIN product p ON oi.product_id p.id GROUP BY p.id, p.name ORDER BY total_qty DESC LIMIT 5;写这三条 SQL 的时候有两个注意点。一是DATE_SUB(CURDATE(), INTERVAL 7 DAY)意味着按自然日统计适合工作台日报场景二是 GROUP BY 后 SELECT 的字段必须是分组字段或聚合函数MySQL 5.7 以上默认开启 ONLY_FULL_GROUP_BY如果 select 里直接写p.name而 group by 只写了p.id会直接报错。解决写法就是上面的GROUP BY p.id, p.name把两个字段都带上。最后给 sale_order.create_time 加普通 B-tree 索引能明显加快按天趋势统计的速度这也是工作台页面首屏渲染卡顿的最常见根治手段。本文还有配套的精品资源点击获取