news 2026/9/28 6:54:42

JSP+SSM农场供销系统源码拆解:架构、事务与部署实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
JSP+SSM农场供销系统源码拆解:架构、事务与部署实战

简介:一套面向农场管理人员和Java初学者的xx农场供销一体化系统源码及说明文档,代码基于SSM(Spring+SpringMVC+MyBatis)与JSP技术栈,结合MySQL数据库与Maven构建工具,实现农产品信息管理、分类管理、在线订购、会员管理及配送等核心功能。系统按角色分为管理员端与会员端,管理员可维护商品与分类、处理订购和会员数据,会员则能浏览农产品并提交订购需求。压缩包约23.55MB,内部源码与说明文档可配合IDEA或Eclipse进行部署,数据库工具可使用Navicat或SQLyog。资源已吸引104人学习,适合用来理解SSM各层整合、JSP动态页面开发、数据库表结构设计以及Maven项目管理流程。通过阅读源码和文档,读者能掌握一套完整的农场供销业务闭环与前后端交互实现,为课程设计或毕业设计提供可直接改造的参考。

1. 农场供销一体化这套源码,核心是 JSP + SSM 的分层协作

“农场供销一体化系统”这个名字听着像期末课设,拆开看就是一套标准的 java SSM + JSP 后台项目:Spring 管业务 Bean,SpringMVC 接 HTTP 请求,MyBatis 读写 MySQL,页面用 JSP 渲染。它要解决的业务不算大但很完整——农资采购入库、农产品销售出库、库存台账、结算对账,四个环节在一个系统里闭环。这套源码最值得看的不是页面做得多精致,而是三层架构怎么把供销单据串成一条可对账的数据流。

它适合三类人:拿 jsp 做毕设选题的学生,想搞懂 SSM 三者怎么协作的 java 学习者,以及要给农场或合作社搭内部管理系统的开发。源码本身能跑是基础,真正有价值的是你能照着它的分层,把供销业务的前因后果讲清楚,然后把数据流改造成自己的。

2. 先拆工程骨架:SSM 与 JSP 在供销系统里各管哪一段

2.1 从 Maven 工程目录看三层职责与 JSP 的落位

拿到源码先别急着点运行,先把目录扫一遍。这类项目通常是标准的 Maven 多模块或单模块结构,webapp 是 JSP 的家,java 包下面按 controller、service、mapper、entity 分层。我一般会先看有没有src/main/webapp/WEB-INF/jsp这个目录,有它说明视图层走的是 JSP 转发,不是前后端分离。

farm-supply-chain ├── pom.xml ├── src/main/java/com/farm │ ├── controller # SpringMVC 的 C:接收请求、校验参数、决定跳哪个 JSP │ │ ├── LoginController.java │ │ ├── GoodsController.java │ │ └── SalesController.java │ ├── service # 业务层:采购、销售、库存扣减的事务在这里 │ │ ├── GoodsService.java │ │ └── SalesService.java │ ├── mapper # MyBatis 的接口层,只管 SQL 映射 │ │ ├── GoodsMapper.java │ │ └── SalesMapper.java │ ├── entity # 与数据库表一一对应的 POJO │ │ ├── Goods.java │ │ └── SalesOrder.java │ └── common # 分页封装、统一返回结果、异常类 └── src/main/resources ├── jdbc.properties ├── spring-context.xml # Spring 容器:数据源、事务、Mapper 扫描 ├── spring-mvc.xml # SpringMVC:视图解析、Controller 扫描 └── mapper/GoodsMapper.xml # SQL 语句集中地 src/main/webapp ├── WEB-INF │ ├── web.xml # Servlet 容器入口,版本号决定 EL 是否默认开 │ └── jsp │ ├── login.jsp │ └── goods/list.jsp

这个结构在 java 课程设计和毕设里出现频率极高,面试问 SSM 项目也喜欢从这棵树开始让你讲。业务上,controller 不写 SQL,mapper 不写 if 判断,service 才是供销规则的执行者。判断一个 SSM 项目写得好不好,先看 service 层是不是足够厚。

2.2 把一张销售单变成一次事务:Controller、Service、Mapper 怎么接力

以“前台提交一张销售订单”为例,完整链路是 JSP 表单 POST 到 Controller,Controller 收参数后调用 Service,Service 先扣库存再插单据,最后返回视图让 JSP 展示结果。这里最容易写错的是把库存扣减写在 Controller 里,然后事务注解失效。

@Controller @RequestMapping("/sales") public class SalesController { @Resource private SalesService salesService; @RequestMapping("/create") public String create(SalesOrder order, Integer goodsId, Integer quantity, Model model) { try { salesService.createOrder(order, goodsId, quantity); model.addAttribute("msg", "下单成功"); } catch (BizException e) { model.addAttribute("msg", e.getMessage()); } return "sales/result"; } }

Controller 里只做了三件事:接收表单参数、调用 service、把结果消息放进 Model 供 JSP 读取。注意Integer goodsId和Integer quantity是 JSP 表单里 input 的 name 值,SpringMVC 会自动绑定,不用手动 request.getParameter。这样写的意义是 Controller 变薄,出错时你能快速定位是参数问题还是业务问题。

再往下看 Service。供销系统的核心逻辑都压在 service 层,尤其是库存扣减和订单生成的原子性。

@Service public class SalesServiceImpl implements SalesService { @Resource private GoodsMapper goodsMapper; @Resource private SalesMapper salesMapper; @Transactional(rollbackFor = Exception.class) public void createOrder(SalesOrder order, Integer goodsId, Integer quantity) { int rows = goodsMapper.deductStock(goodsId, quantity); if (rows == 0) { throw new BizException("库存不足"); } order.setTotalAmount(order.getPrice().multiply(new BigDecimal(quantity))); salesMapper.insertOrder(order); SalesItem item = new SalesItem(); item.setOrderId(order.getId()); item.setGoodsId(goodsId); item.setQuantity(quantity); item.setAmount(order.getPrice().multiply(new BigDecimal(quantity))); salesMapper.insertItem(item); } }

这段代码解释了“供销一体化”里库存和订单为什么必须在一件事里完成。@Transactional(rollbackFor = Exception.class)表示任何异常都要回滚,包括受检异常;deductStock返回 0 表示库存扣减失败,直接抛异常,后面的 insert 就不会执行。如果这里没用事务,会出现库存扣了但订单没生成的黑匣子账目。

2.3 依赖清单与版本边界:这套组合的默认安全区

SSM 的版本搭配在 java 面试题里几乎是个固定考点,实际跑项目时选错版本比写错代码更致命。常见做法是 JDK 8 + Spring 5.2.x + MyBatis 3.5.x + MyBatis-Spring 2.0.x,Tomcat 用 8.5 或 9。这套组合经过大量项目验证,是源码能直接跑起来的最大公约数。

<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.22.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.10</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> </dependencies>

注意两个细节:mysql 驱动的 scope 是 runtime,编译期用不到,运行时由 Tomcat 加载;servlet-api 必须 provided,否则和 Tomcat 自带的冲突,启动直接报 NoSuchMethodError。如果你本机是 MySQL 5.7,驱动版本可以降到 5.1.49,但更省事的做法是库和驱动都统一到 8.0,url 里加时区参数即可。

3. 数据库建模与供销闭环:库存、订单、结算怎么串成一条线

3.1 供销核心表概览:把业务拆成可对账的关系

农场供销系统的复杂度不在技术,在表结构设计。设计目标是让“采购进来多少、卖出去多少、还剩多少”三件事能互相印证。下面是核心表的分工,字段以精简够用为准。

表名职责关键字段与供销的关系
goods商品档案goods_name、category、unit、price、status供销的对象,上下架状态
supplier供应商name、contact、phone采购来源
warehouse_stock库存台账goods_id、quantity采购加、销售减,实时余额
purchase_order采购单order_no、supplier_id、total_amount、status入库依据
purchase_item采购明细order_id、goods_id、price、quantity每笔采购的具体商品
sales_order销售单order_no、customer、total_amount、status出库依据
sales_item销售明细order_id、goods_id、quantity、amount每笔销货的具体商品
settlement结算对账order_no、order_type、amount、status采购与销售统一记账

这个拆分思路是“主表 + 明细表”模式,采购单和销售单都需要流水号,明细表记录商品级数据。库存表是中间的账本,采购单审核通过给库存加,销售单创建成功给库存减,结算表再做金额层核对。三层对上了,供销才不会出现账实不符。

3.2 建表 SQL:明细表为什么要冗余商品名称和单价

看建表语句能判断作者有没有实战经验。一个典型的 sales_item 表会把 goods_name 和 price 冗余进来,而不是查询时再去 join goods 表。原因是 JSP 列表页经常要展示历史单据,单据一旦生成,商品改名、调价都不该影响它。

CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, goods_name VARCHAR(64) NOT NULL, category VARCHAR(32) NOT NULL DEFAULT '蔬菜', unit VARCHAR(8) NOT NULL DEFAULT 'kg', price DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE warehouse_stock ( goods_id INT PRIMARY KEY, quantity INT NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES goods(id) ); CREATE TABLE sales_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, customer VARCHAR(64) NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT '0待结算 1已结算 2已取消', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE sales_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, goods_name VARCHAR(64) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, amount DECIMAL(10,2) NOT NULL, KEY idx_order (order_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES sales_order(id) );

这里三个设计点值得抄:金额字段统一用 DECIMAL,禁止 float 和 double,否则结算对账会因精度翻车;sales_item 冗余 goods_name 和 price,让 JSP 展示订单详情时少两张表 join;warehouse_stock 以 goods_id 为主键,天然保证一个商品只有一行库存记录,并发扣减时好锁。

3.3 扣库存不靠先查再减:一条 UPDATE 的乐观锁语义

很多人第一次写库存扣减是先 SELECT 查 quantity,判断够不够再 UPDATE。这套写法在单用户调试时看不出来,一旦两个人同时下单就超卖。正确做法是把判断写在 UPDATE 的 WHERE 条件里,让数据库替你做原子校验。

UPDATE warehouse_stock SET quantity = quantity - #{quantity} WHERE goods_id = #{goodsId} AND quantity >= #{quantity}

MyBatis 中这条 SQL 的返回值就是 affected rows。如果库存够,返回 1;不够,返回 0,Service 层拿到 0 直接抛业务异常。它不依赖应用锁,也不需要先 SELECT,性能和对错两头都占。采购入库同理,改成quantity = quantity + #{quantity},但要考虑并发下同一商品多笔采购单同时入库,用追加而不是赋值覆盖。

3.4 初始化数据:别让外键和状态字段卡住第一次启动

源码的说明文档一般会附 SQL 脚本,但常见坑是只建表不给初始化数据。跑供销系统至少需要一条管理员账号、两类商品、一行初始库存,否则登录进去全是空列表,你根本分不清是功能没实现还是数据没喂进去。

INSERT INTO goods (id, goods_name, category, unit, price, status) VALUES (1, '有机西红柿', '蔬菜', 'kg', 5.50, 1), (2, '农资复合肥', '农资', '袋', 120.00, 1); INSERT INTO warehouse_stock (goods_id, quantity) VALUES (1, 200), (2, 50); INSERT INTO sys_user (id, username, password, real_name) VALUES (1, 'admin', MD5('123456'), '系统管理员');

注意密码别用明文,MD5 是这套老项目的常见做法,虽然不够强但至少不会一眼泄露。status 字段的取值要在代码里统一:商品 1 上架 0 下架,订单 0 待结算 1 已结算 2 已取消。JSP 页面上显示中文状态时通常在 controller 里做映射,不要在页面写死。

4. 跑通源码的关键配置:Idea 导入、Spring 整合、JSP 联调

4.1 两种导入方式:直接开 Maven 工程,还是手动建 JSP 项目

拿到源码后第一件事不是改代码,是把工程正确导入 Idea。这里因人而异:如果坚持用“idea 新建 jsp 项目”的传统方式,需要手动把 web 目录指到 src/main/webapp,再配置 Artifacts;更省事的是直接用 Maven 方式打开。

常见做法是 Idea 里 File -> Open,选择项目下的 pom.xml,以 Maven 项目导入。等待依赖下载完成后,检查右侧 Maven 窗口有没有出现 spring、mybatis 相关依赖,然后配置 Tomcat:Run -> Edit Configurations -> 新建 Tomcat Server Local,Deployment 里添加 Artifact。这里有个区分:如果它显示 war exploded,说明是开发模式,改 JSP 可以热更新;如果是 war 包,每次都要重新构建,适合最终部署。

提示:老项目常见的问题是本地 Maven 仓库没有对应版本依赖,idea 会一直卡在下载。把 Maven 的 settings.xml 里的镜像仓库配好,再重新 import 一次,基本能解决。

4.2 jdbc.properties 与 spring-context.xml:数据源、事务、Mapper 扫描三件事

数据库连不上是启动失败的头号原因,而大部分是因为驱动类名和时区参数。记住 MySQL 8 用com.mysql.cj.jdbc.Driver,MySQL 5 用com.mysql.jdbc.Driver,url 里必须加serverTimezone=Asia/Shanghai,否则报时区错误。

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/farm?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码

Spring 配置文件里要做的三件事:数据源、SqlSessionFactory、事务管理器。顺序不能乱,事务管理器依赖数据源,SqlSessionFactory 依赖 mapperLocations 指向 XML。

<context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.farm.mapper"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>

这里的classpath:mapper/*.xml是高频出错点:如果你的 Mapper XML 放在 resources 之外的目录,或者路径写成了classpath*:,MyBatis 就找不到 SQL,运行时直接报 Invalid bound statement。MapperScannerConfigurer 的 basePackage 是接口所在包,它会把包下所有接口自动代理成 Mapper Bean。

4.3 spring-mvc.xml:视图解析器决定 JSP 跳到哪里

SpringMVC 与 JSP 协作的关键是 InternalResourceViewResolver。它负责把 Controller 返回的字符串视图名,解析成真正可访问的 JSP 路径。如果这个前缀后缀配置不对,任何请求都会 404。

<mvc:annotation-driven/> <context:component-scan base-package="com.farm.controller"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <mvc:resources mapping="/static/**" location="/static/"/> <mvc:resources mapping="/upload/**" location="/upload/"/>

/WEB-INF/jsp/前缀的含义是把 JSP 藏在 WEB-INF 下,外部浏览器直接输入 URL 访问不到,必须经 Controller 转发。这是 SSM 项目的标准安全姿势,也能避免 JSP 源码直接暴露。静态资源单独配置是防止 DispatcherServlet 把 css/js 也当 controller 请求处理,不加这段页面会全部裸奔。

4.4 web.xml:servlet 版本与编码过滤器不能省

web.xml 是第一道关卡,版本声明直接关系到 JSP 的 EL 表达式是否默认开启。如果你的 web.xml 是 2.3 或 2.4 的老版本,JSP 页面里的${goods.name}会被原样输出,原因就是 EL 功能默认关闭或部分支持。用 Servlet 3.1 版本声明可以从根上避开这个坑。

<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <filter> <filter-name>encoding</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encoding</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>

CharacterEncodingFilter 必须放在所有过滤器最前面,且 url-pattern 写/*,否则 JSP 提交的中文表单数据到后端就是乱码。DispatcherServlet 的 url-pattern 用/,表示除了.jsp之外的所有请求都进 SpringMVC;.jsp请求由 Tomcat 的 Jasper 解析,这样 JSP 页面本身不走 Controller 也能被访问到。

4.5 跑通一次完整供销链路:从登录到库存变化

配置无误后,启动 Tomcat,浏览器访问http://localhost:8080/farm/,正常会跳登录页。用 admin 登录之后,按采购入库、销售出库、库存查询的顺序走一遍,验证标准是:仓库里商品数量先增加后减少,结算页面的金额与订单明细一致。

我一般会准备一张草稿纸,把初始库存数写下来,每做一笔就核对一次页面数字。如果下单后库存没变,优先怀疑事务没生效或者 Mapper XML 的 update 语句写成了 insert。这一步跑通,比看十页文档都管用,因为它一次性验证了 Spring 容器、MyBatis 映射、JSP 视图解析三条链路。

5. 排查与避坑:JSP 项目跑不起来时,先查这五个地方

5.1 页面把${goods.name}原样打印出来

现象:JSP 页面没有报错,但 EL 表达式没有被解析,页面上直接显示${goods.name}这类原文。

原因:绝大多数是 web.xml 的 servlet 版本声明太低。Servlet 2.3 及以下默认 isELIgnored 为 true,2.4 之后才默认支持 EL 解析。如果 web.xml 头声明的是 2.3 版本,或者压根没有版本声明被容器当成老版本处理,就会出现整页表达式裸奔。

解决:把 web.xml 的 version 改成 3.0 或 3.1,schema 换成xmlns.jcp.org的 javaee 版本。改完记得 Rebuild,Maven 项目里 WEB-INF/web.xml 是资源文件,不重新构建可能还在用旧的。

5.2 数据库连接报 Communications link failure

现象:Tomcat 启动时 Spring 初始化数据源失败,控制台出现Communications link failure或Access denied for user。

原因:驱动类名不匹配。MySQL 8 的驱动类已经改成com.mysql.cj.jdbc.Driver,连接串少了serverTimezone=Asia/Shanghai也会直接失败。如果 pom 里引入的 mysql-connector-java 是 5.x,而数据库是 8.x,协议层就不兼容,怎么配都连不上。

解决:先把数据库版本确认清楚,再统一驱动。MySQL 8 配mysql-connector-java 8.0.x和com.mysql.cj.jdbc.Driver,并在 url 后面拼?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai。这个坑最常见于本地装的是 8.0,但网上搜到的老教程都是 5.x 写法。

5.3 MyBatis 报 Invalid bound statement (not found)

现象:项目能启动,Controller 也能进,但一调用 Mapper 接口的方法就抛Invalid bound statement (not found): com.farm.mapper.GoodsMapper.getGoodsById。

原因:Mapper 接口和 XML 没有绑定成功。常见三种情况:XML 文件的 namespace 和接口全限定名不一致;XML 没被mapperLocations扫到,比如放在了src/main/java下面而不是 resources;接口方法名与 XML 里的 statement id 对不上。

解决:先检查 namespace 是否等于接口的全路径,例如com.farm.mapper.GoodsMapper;再确认 spring-context.xml 里mapperLocations路径是classpath:mapper/*.xml;最后看方法名和 id 是否一字不差。改完 spring 配置必须重启,Mapper 绑定是在容器启动时完成的,热部署救不了。

5.4 修改 JSP 后不生效,运行的还是旧页面

现象:改了 JSP 的标题或表格列,刷新浏览器没有任何变化,甚至重启 Tomcat 还是老页面。

原因:JSP 是动态编译的,Tomcat 的 work 目录缓存了上一次编译的 Java 文件和 class 文件。开发模式下 idea 没有把 resources 更新到 target,或者浏览器强缓存了 .jsp 响应。另一个隐蔽原因是代码里对 JSP 响应设置了Cache-Control头,导致浏览器端走了本地缓存。

解决:Idea 里 Build -> Rebuild Project,把 target 目录清理重建;Tomcat 的 deployment 选 war exploded 并打开 On frame deactivation 的 Update classes and resources;浏览器按 Ctrl+F5 强制刷新。如果还不行,手动删掉 Tomcat 的work/Catalina目录再启动。

5.5 Nginx 代理后页面能开但接口全部 404

现象:直接访问 Tomcat 端口一切正常,接入 Nginx 做反向代理后,JSP 页面能打开,但表单提交和列表查询的路径全 404。

原因:JSP 本身由 Tomcat 渲染,Nginx 只是透传,所以页面能出来。404 的通常是/sales/xxx这类 Controller 路径,大方向是请求没转发到 Tomcat 的上下文路径。例如 Tomcat 应用上下文是/farm,而 Nginx 转发时把/farm剥掉了,SpringMVC 收到的请求路径就对不上@RequestMapping。

解决:Nginx 配置里转发时保留原始 URI,不要用 rewrite 去掉前缀。另外要明确一件事:nginx 本身不支持 JSP 解析,所有.jsp页面和/api请求最终都要落到 Tomcat,Nginx 只负责反向代理和静态资源分发。建议把/static/、/upload/这类路径直接用 Nginx 指向磁盘,其余全部proxy_pass到 Tomcat 的 8080。

6. 不推翻 JSP 的改造技巧:先补接口回归、缓存和一张对账 SQL

把 SSM + JSP 这套跑通后,很多人会纠结要不要推到重来换 Spring Boot。我建议先别急,老系统最缺的不是框架,是验证手段。我接手这类供销系统时,第一件事不是重构,而是先给 Service 层补上单元测试,再用一个 JSON 接口做联调,最后写对账 SQL 守住钱账一致。

先给核心 Service 写测试。SSM 项目用 Spring Test 直接注入,不启动 Tomcat 也能验证库存扣减逻辑,这比在页面上点点点高效得多。

@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(locations = {"classpath:spring-context.xml"}) public class SalesServiceTest { @Resource private SalesService salesService; @Test public void testCreateOrderSuccess() { SalesOrder order = new SalesOrder(); order.setOrderNo("SO20250101001"); order.setCustomer("测试客户"); salesService.createOrder(order, 1, 10); // 断言库存从 200 变为 190 } }

然后加一个 JSON 接口,不破坏 JSP 页面,但给后续联调留后门。在 Controller 里用@ResponseBody返回商品列表 JSON,前端可以用 Postman 直接测,不必每次打开浏览器。

@RequestMapping("/api/goods/list") @ResponseBody public Map<String, Object> listGoodsApi() { List<Goods> list = goodsService.listAll(); Map<String, Object> result = new HashMap<>(); result.put("code", 0); result.put("data", list); return result; }

接口验证稳定后,再考虑缓存。JSP 页面访问量上来时,商品列表和分类是热点数据,但注意别缓存库存数字,缓存只放商品基础信息,库存仍实时查库。这段改造是可选的,重点是让系统先有可回归的测试和可对账的 SQL。

最后是守住账目的对账 SQL。跑完十笔订单后,用一条语句核对销售明细金额之和与销售单总额是否一致,再查库存为负数的商品,这两条检查能让供销系统的数据问题在半小时内暴露。

-- 核对销售单总额与明细是否一致 SELECT order_id, SUM(amount) AS item_total FROM sales_item GROUP BY order_id HAVING item_total <> (SELECT total_amount FROM sales_order WHERE id = order_id); -- 找出库存为负数的商品 SELECT goods_id, quantity FROM warehouse_stock WHERE quantity < 0;

老系统的价值不在技术新颖,而在于业务闭环完整可验证。我现在的习惯是:不管接手什么 JSP 老项目,先把对账 SQL 和接口测试补上,再谈重构。希望帮到你。

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

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

AI工程从零实战:手写反向传播到全链路部署实践

最近后台收到不少私信&#xff0c;都在问同一个事儿&#xff1a;非科班、零基础&#xff0c;到底能不能啃下 AI 工程这块硬骨头&#xff1f;刚好我手头就在做一个小项目&#xff0c;代号就叫ai-engineering-from-scratch&#xff0c;意思很直白&#xff0c;就是完全从零开始&am…

作者头像 李华
网站建设 2026/9/28 6:50:08

超轻量AI助手nanobot Docker部署指南:本地模型与WebUI实战

1. 为什么我最终选了 nanobot 而不是其他 AI 助手方案1.1 从一次折腾了三天的部署说起前阵子我想给自己搭一个能长期跑在 NAS 上的个人 AI 助手&#xff0c;需求其实很朴素&#xff1a;能对话、能记住上下文、能挂本地模型、最好有个网页界面&#xff0c;别太吃资源。一开始我试…

作者头像 李华
网站建设 2026/9/28 6:50:08

本地部署AI编程智能体:Ollama与PI-Desktop实操指南

做编程智能体&#xff0c;最麻烦的往往不是模型本身&#xff0c;而是运行环境。把代码交给云端对话窗口跑&#xff0c;每一次生成都在烧 token&#xff0c;代码文件还会留在别人的服务器上。我的思路是把整套链路搬到本地&#xff1a;用 PI-Desktop 这个开源桌面端当智能体运行…

作者头像 李华