☰
Spring Boot城郊蔬菜大棚管理与销售系统开发全解析
2026/10/10 14:02:45 网站建设 项目流程

接手过不少这种项目,一听到“基于Spring Boot的城郊蔬菜大棚管理与销售系统”,我先跟你说句实在话:这活儿看着像个毕业设计,实际上是个特别典型的“管理+交易”全栈小项目。一边是大棚里的环境数据、种植记录,另一边是商品上架、订单库存,两边业务逻辑差挺远,却要在一个系统里跑通。我自己做过几轮类似的东西,踩过不少坑,这次把完整的拆解思路、核心代码、部署讲解和常见问题一次性整理出来,供你参考。

这套系统的关键词很清楚:Spring Boot、源码、LW(也就是配套文档/论文)、部署讲解。也就是说,它不只是给你一套能跑的代码,而是从设计到实现、从本地调试到上线部署,全链路都得说得明白。我会按“需求拆解 → 模块设计 → 数据库实现 → 部署实战 → 问题排查”这个顺序来讲,尽量说人话,把每一步背后的为什么也讲清楚。

1. 项目整体设计与思路拆解

1.1 这个系统到底在解决什么问题

城郊蔬菜大棚的运营,跟你在写字楼里写代码完全是两个节奏。棚主关心的是:我这个棚里现在温度多少、湿度合不合适、要不要浇水施肥;同时他又得操心种出来的菜能不能卖出去、卖给谁、卖多少钱。传统做法是拿个本子记,环境靠人眼看,销售靠电话联系批发商,效率极低。你去看任何一个城郊蔬菜基地,大概率能看到墙上贴着一堆纸质记录单——这就是系统的切入点。

所以你别把这个项目理解成一个简单的CRUD,它的核心其实是两个业务闭环的叠加:

  • 种植管理闭环:大棚信息 → 环境监测数据 → 种植任务记录 → 农事提醒。
  • 销售管理闭环:商品信息 → 前端展示 → 用户下单 → 订单处理 → 库存扣减。

两个闭环之间靠“大棚-种植批次-商品”这条线串联起来。比如2号大棚种了一茬西红柿,这批西红柿对应一个商品“有机西红柿”,库存数量跟着大棚产量走。这个关联逻辑如果没设计好,后面做订单、做库存的时候就会非常痛苦。

1.2 技术选型:为什么是Spring Boot

选Spring Boot不是跟风,是它确实契合这种项目的场景。先说优点:

  • 快速搭建:Spring Boot的自动配置极大简化了SSM时代那堆XML配置,一个@SpringBootApplication注解就能跑起来,适合单人开发或小团队。
  • 生态成熟:MyBatis-Plus、Spring Security、Redis这些常用组件都有现成的starter,接起来很快。
  • 部署友好:打包成可执行Jar,服务器上装个JDK16/17就能跑,不用再像传统Web项目一样配Tomcat。这对“源码+部署讲解”的交付方式太重要了——你总不能要求棚主去手动挂载一个war包吧?

当然它也有弱点,比如大并发场景下的性能表现一般、内存占用偏高。但这个项目的体量决定了它根本不需要分布式那套东西,单机单库完全够用。你非得上微服务、上Kafka,反而把简单问题复杂化。

1.3 系统架构与模块划分

我用的是经典的前后端分离思路,但考虑到很多同学第一次做这种完整项目,我建议你分两层看:

后端:Spring Boot 2.7 + MyBatis-Plus + MySQL 8.0。按业务拆controller、service、mapper三层,包结构大概是:

com.example.greenhouse ├── controller // 接口层,只管接收参数和返回结果 ├── service // 业务逻辑层,核心逻辑都在这里 ├── mapper // 数据访问层,配合MyBatis-Plus ├── entity // 数据库实体类 ├── vo // 视图对象,专门给前端返回的数据结构 ├── config // 配置类,比如跨域、拦截器、静态资源映射 └── common // 通用返回类、异常处理、工具类

前端:可以选Vue + Element UI,也可以直接用Thymeleaf模板。我的建议是:如果你要的是“快速交付、部署讲解简单”,那就用Vue打包后扔进Spring Boot的static目录;如果你后续还要自己维护,那前后端分离、单独部署更舒服。这两种方式对应的部署讲解差别很大,后面我会专门讲。

系统功能上,我按角色把用户分成三类:

  • 管理员:管理所有大棚、商品、订单、用户,查看统计报表。
  • 种植人员:录入大棚环境数据、维护种植记录、处理农事任务。
  • 普通用户/采购商:浏览商品、下单、查看订单状态。

这套权限模型不需要Spring Security那种重量级方案吗?其实用拦截器加个简单的角色判断就够了。真上了Spring Security,配置不好还会把登录流程搞得很难调试,对于这种项目反而增加负担。用拦截器是性价比最高的做法。

2. 核心功能模块解析与实操要点

2.1 大棚环境管理模块的细节设计

大棚环境管理是很多新手最容易做“飘”的地方,容易写成一堆数字的增删改查。真正实用的大棚模块,至少要有这几层设计:

  • 基础档案:大棚编号、名称、面积、位置、类型(温室/拱棚/连栋棚)、当前种植作物。这些字段别瞎起名,建议直接用greenhouse_code、area、location这种见名知义的。
  • 环境数据记录:温度、湿度、光照强度、土壤湿度、二氧化碳浓度。数据量大且是时序性的,建议用sensor_data表单独存放,通过greenhouse_id关联大棚。
  • 环境状态判断:这是容易忽略的一点。后端不仅要存原始数值,最好再加一个status字段,比如温度超过35度就标记为“高温预警”。这样前端界面就可以直接展示红黄绿状态,而不是让用户看一堆数字自己判断。
  • 数据录入方式:由于没有真实物联网硬件,一般做法是模拟接口或者后台手工录入。我建议做一个“一键模拟生成”的功能,点击后自动生成最近几小时的环境数据,方便演示和自测。

这里有个很关键的细节:环境数据表不要做一个“最新状态”字段去覆盖旧数据。我之前见过有人把环境数据设计成单条记录,每次新数据来了就UPDATE,结果历史趋势图根本画不出来。正确做法是每次采集都INSERT一条新记录,查询最新状态时按时间倒序取第一条,或者写一个子查询取MAX(create_time)对应的记录。

测温湿度的阈值判断逻辑,拆出来单独做成一个Service方法,比如checkEnvironmentStatus(Long greenhouseId),在里面根据各项指标综合判断当前大棚状态。这样以后要调整预警规则,只改这个方法就行,不用翻整个Controller。

2.2 种植记录与农事任务管理

种植记录说白了就是给大棚建立“病历本”:种了什么种子、什么时间播种、什么时候施肥、打了什么农药、预计哪天采摘。这个模块的价值在于,等蔬菜上市时能知道每一批货的来源和农事过程,说白了就是“溯源”。

做这块的时候,我建议你把“种植批次”这个概念单独抽出来。一个大棚可以多次种植,一次种植算一个批次,每个批次对应一个plant_batch记录。字段包括:

  • 大棚ID、作物名称、品种、播种日期、预计采收日期、实际采收日期;
  • 种植状态(生长中、已完成、已采收);
  • 负责人。

农事任务可以做成一个简单的待办列表,字段有:任务标题、任务类型(浇水/施肥/打药/除草)、所属批次、计划时间、完成状态、操作人。这样可以让种植人员登录后看到自己需要处理的农事任务,处理完打个勾。整个逻辑不难,但非常贴近实际场景。

我踩过的一个坑是:农事任务的时间提醒依赖定时任务,但很多人不会配Spring的@Scheduled。其实很简单,启动类加@EnableScheduling,然后在任务检查方法上加@Scheduled(cron = "0 0 8 * * ?"),每天早上8点扫一遍今天到期未完成的任务,主动推一条站内信或提醒记录。做这个功能之前,先确认你是否真的需要“主动推送”,不然写成“查询今日任务”就够了。

2.3 销售与订单模块:库存扣减必须用事务和锁

销售模块是这个系统里跟钱挂钩的部分,不能有一点马虎。业务流程是这样的:

前端用户把商品加入购物车,确认订单,填写收货地址,提交支付(这里一般是模拟支付),生成订单,然后后台管理员看到订单后发货。库存呢?在用户下单那一刻就应当扣减,否则就会出现“两个人都下单成功但库存只有1份”这种严重问题。

扣库存的代码实现,我建议在Service层加@Transactional注解,同时用乐观锁或悲观锁控制并发。最简单的方案是:更新库存前查一次,扣减时用UPDATE product SET stock = stock - #{num} WHERE id = #{id} AND stock >= #{num},这样一条SQL就能原子性地扣库存。MyBatis-Plus里可以这么写:

@Override @Transactional(rollbackFor = Exception.class) public boolean createOrder(OrderCreateRequest request) { // 先校验商品状态和库存 Product product = productMapper.selectById(request.getProductId()); if (product == null || product.getStatus() != 1) { throw new BizException("商品不存在或已下架"); } // 关键:库存扣减使用原子更新 int rows = productMapper.deductStock(product.getId(), request.getQuantity()); if (rows == 0) { throw new BizException("库存不足"); } // 生成订单主记录 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(request.getUserId()); order.setTotalAmount(product.getPrice() * request.getQuantity()); order.setStatus(0); // 0待支付 1已支付 2已发货 3已完成 4已取消 orderMapper.insert(order); // 生成订单明细 OrderItem item = new OrderItem(); item.setOrderId(order.getId()); item.setProductId(product.getId()); item.setQuantity(request.getQuantity()); item.setPrice(product.getPrice()); orderItemMapper.insert(item); return true; }

细节上,要注意订单编号的生成方式。用数据库自增ID当订单号绝对不行,太容易被人猜到订单量了。我习惯用时间戳加随机数:yyyyMMddHHmmss + 6位随机数。如果多个方法都要生成,可以抽一个OrderNoGenerator工具类。

还有一点,跟钱相关的业务,金额字段不要用float/double,数据库里用DECIMAL(10,2),Java里用BigDecimal。第一次用的时候我拿double算金额,算来算去少了0.1元,被测试当场抓到。用BigDecimal虽然啰嗦点,但安全得多。

2.4 用户、角色与权限控制

权限控制这块,我前面提过用拦截器,不搞Spring Security。简单说下思路:

用户表里加一个role字段,0=管理员,1=种植人员,2=采购商/普通用户。登录成功后,把用户信息和角色放进Session。自定义一个AuthInterceptor,在preHandle里去判断当前请求的路径是否需要登录、是否需要特定角色。

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User currentUser = (User) session.getAttribute("currentUser"); String uri = request.getRequestURI(); if (uri.startsWith("/user/login") || uri.startsWith("/user/register")) { return true; // 放行登录注册 } if (currentUser == null) { // 如果是Ajax请求,返回401状态码;否则重定向到登录页 response.setStatus(401); return false; } // 角色校验,比如 /admin/** 开头需要role=0 if (uri.startsWith("/admin/") && currentUser.getRole() != 0) { response.setStatus(403); return false; } return true; } }

拦截器注册在WebMvcConfig里:

@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/user/login", "/user/register", "/static/**", "/error"); } }

不选Spring Security的原因也说一下:新手容易把过滤器链配错,出现“登录接口本身都被拦截了”“放行路径不生效”这类难以排查的问题。拦截器的逻辑直观得多,完全够这个体量的项目使用。等哪天真要做复杂安全体系了,再去啃Spring Security,那个学习曲线值得单独开一篇讲。

3. 数据库设计与核心实现

3.1 核心表结构设计

数据库是整个系统的地基,表建得对不对直接决定后面写代码的顺畅程度。我把这套系统用到的表全部列在下面,建表时可以直接参考:

表名用途关键字段
user用户表username, password, phone, role, status
greenhouse大棚信息表greenhouse_code, name, area, location, type, status
sensor_data环境数据表greenhouse_id, temperature, humidity, light, soil_humidity, co2, status, create_time
plant_batch种植批次表greenhouse_id, crop_name, seed_time, plan_harvest_time, actual_harvest_time, status
farm_task农事任务表batch_id, task_type, description, plan_time, status, operator_id
product商品表name, category, price, stock, unit, image, description, status
cart购物车表user_id, product_id, quantity
order订单表order_no, user_id, total_amount, status, address, create_time
order_item订单明细表order_id, product_id, quantity, price
notice消息通知表user_id, title, content, is_read, create_time

表字段统一规范:主键都是id,创建时间都叫create_time,更新时间叫update_time,逻辑删除统一用deleted字段(0未删,1已删)。这样做的好处是:MyBatis-Plus里配置一次全局策略,所有表通用,不用每个实体单独写注解。

订单和订单明细为什么要拆两张表?因为一个订单可能包含多种商品。虽然很多小项目简化成了一张表存多个商品ID,但那种设计做订单统计和售后退款时非常难受。拆表虽然多写一点代码,但以后扩展无障碍。这就跟我们写代码要开闭原则一个道理。

3.2 用MyBatis-Plus减少大量重复代码

这套系统用了MyBatis-Plus之后,CRUD的代码量能降低一半以上。实体类直接继承BaseMapper:

@Data @TableName("greenhouse") public class Greenhouse { @TableId(type = IdType.AUTO) private Long id; private String greenhouseCode; private String name; private BigDecimal area; private String location; private Integer type; private Integer status; }

然后Mapper接口只写一行:

@Mapper public interface GreenhouseMapper extends BaseMapper<Greenhouse> { // 增删改查、分页查询全继承了 }

Service层里,条件查询用LambdaQueryWrapper非常顺手,比如查某个大棚最新一条环境数据:

// 查2号大棚最新一条环境记录 SensorData sensorData = sensorDataMapper.selectOne( new LambdaQueryWrapper<SensorData>() .eq(SensorData::getGreenhouseId, 2L) .orderByDesc(SensorData::getCreateTime) .last("limit 1") );

这个.last("limit 1")是个小技巧,虽然看起来有点“侵入”,但用来取最新一条记录确实方便。如果你想写得更规范,也可以用自定义SQL加@Select注解,这里其实是个取舍,我自己的习惯是MyBatis-Plus处理简单查询,复杂统计和联表查询才写XML。

3.3 环境数据统计与展示实现

前端展示大棚环境趋势图,一般用ECharts,后端只需要提供一个按时间范围查询数据的接口。对应SQL大概是:

SELECT date_format(create_time, '%H:00') AS hour_point, AVG(temperature) AS avg_temp, AVG(humidity) AS avg_humidity FROM sensor_data WHERE greenhouse_id = #{greenhouseId} AND create_time BETWEEN #{startTime} AND #{endTime} GROUP BY date_format(create_time, '%H:00') ORDER BY hour_point;

一个必须注意的坑:时间分组时,如果MySQL和Java应用的时区不一致,查询结果会偏移8小时。JDBC连接串上一定要加serverTimezone=Asia/Shanghai,否则你本地看数据是准的,上了服务器就发现数据线整体挪了一截,特别容易误判成代码逻辑错了。

这个统计SQL在MyBatis-Plus里用自定义Mapper方法写比较自然,如果你不想写XML,也可以用@Select注解写在Mapper接口里。接口层返回的是List<Map<String, Object>>,前端拿到数据后稍微加工一下就能喂给ECharts。分组统计的结果别用实体接收,因为里面是聚合字段(avg_temp),对应不上任何一张表的列。

4. 项目部署实战

4.1 本地打包的正确姿势

一个能交付源码的项目,部署讲解是最出戏的部分。很多同学代码写完了,但别人拿到手怎么也跑不起来,问题往往出在“本地能跑”和“别人能跑”之间差了一个完整的部署说明。我强烈建议交付文档里至少包含这几块:

  • 环境要求:JDK 1.8/11、Maven 3.6+、MySQL 8.0;
  • 数据库初始化脚本(建库建表SQL + 初始数据);
  • 配置文件修改说明(数据库账号密码、端口等);
  • 打包命令和运行命令。

打包之前,先把配置文件分环境整理好。用Spring Boot的spring.profiles.active机制,放application-dev.yml和application-prod.yml两套配置。本地用dev,服务器用prod,两者的数据库地址、日志级别都不一样。这样切换环境就是启动参数加--spring.profiles.active=prod,不用改代码。

打包执行:

mvn clean package -DskipTests

打包成功后,在target目录下会生成一个xxx.jar包。我每次交付前都会验证两个事:第一,是不是能直接java -jar跑起来,不依赖IDE;第二,启动时日志里有没有明显报错,比如端口占用、数据库连接失败。这两个验证通过,说明打包没问题。

4.2 Linux服务器部署实操

服务器上部署,我的标准操作流程是这样的:

  1. 上传Jar包:用scp、rsync,或者直接在服务器上拉取。注意上传后赋予执行权限:
chmod +x greenhouse-system.jar
  1. 启动:先用前台模式启动一次,确认能正常起来:
java -jar greenhouse-system.jar --spring.profiles.active=prod

看到Started Application in X seconds再按Ctrl+C停掉,然后改用后台运行:

nohup java -jar greenhouse-system.jar --spring.profiles.active=prod > app.log 2>&1 &
  1. 检查状态:jps命令看Java进程在不在;tail -f app.log看日志输出;curl http://localhost:8080/api/health看接口是否响应。

  2. 配置Nginx:如果前端是Vue单独部署,Nginx做静态文件服务和API反向代理。这里有个老生常谈但容易配错的点,location /api/要设置proxy_pass http://127.0.0.1:8080;,并且注意结尾的斜杠问题,少一个斜杠会把/api前缀也带过去了。

用systemd管理服务呢?这个是生产环境更正规的做法。在/etc/systemd/system/greenhouse.service里写一个Unit文件:

[Unit] Description=Greenhouse System After=network.target [Service] Type=simple User=root ExecStart=/usr/bin/java -jar /opt/greenhouse/greenhouse-system.jar --spring.profiles.active=prod Restart=on-failure RestartSec=10 [Install] WantedBy=multi-user.target

然后用systemctl daemon-reload、systemctl start greenhouse、systemctl enable greenhouse三条命令,就实现了开机自启和崩溃自动重启。这比裸用nohup稳得多,日志也可以直接用journalctl -u greenhouse -f查看,非常方便。

4.3 前端Vue打包进Spring Boot的合并部署方案

前面提到过,前端可以打包进Spring Boot的static目录,实现单端口合并部署。这个方案特别适合“源码+部署讲解”的交付,因为对服务器要求最低,用户只需要跑一个进程。

操作方法是:前端项目执行npm run build,会生成dist目录;把dist里的内容拷贝到Spring Boot项目的src/main/resources/static下;重新打包。但这里有几个细节:

  • 前端项目里API请求的baseURL必须用相对路径,比如/api,不能写http://localhost:8080/api,否则打包后接口地址写死,换了服务器就废了。
  • Vue Router如果用的history模式,刷新页面会404。解决方法是后端加一个Controller做页面路由转发,或者把Vue路由模式改成hash模式(URL里带#号)。我图省事直接用了hash模式,反正这不是C端对外产品,URL难看一点无所谓。
  • 静态资源与后台接口路径不要冲突。/api/**给接口,/static/**或直接根路径给前端静态资源,Spring Boot默认静态资源配置不需要额外改,只要接口路径不在static目录范围内就行。

这种合并部署的优点是简单,缺点是前端更新要重新打包后端。如果后端和前端是两个人分别维护,这种模式就会很痛苦。所以你要根据实际交付对象来选择。

5. 常见问题与排查技巧实录

5.1 项目跑不起来:端口、连接、版本三座大山

我帮别人排查这个系统跑不起来的问题,排在最前面的永远是这三个:

  • 端口被占用:Spring Boot默认8080,如果本机装了别的服务占用了,启动直接报Port 8080 was already in use。检查方式:netstat -tlnp看谁占着端口。解决方式:重启占用端口的进程,或者改配置server.port=8081。
  • 数据库连接不上:报错信息里有Communications link failure或Access denied for user。前者查IP、端口、useSSL=false;后者查账号密码、数据库授权。我在部署文档里会特别提醒,MySQL 8.0要用com.mysql.cj.jdbc.Driver,driver-class-name别照抄5.7的旧值。
  • Java版本不匹配:Spring Boot 2.7最低要求JDK8,但如果你用了某些新特性或者依赖强制要求JDK11/17,本地和服务器版本不一致就会出各种诡异的ClassNotFoundException。最好的办法是服务器和本地用同一个JDK主版本,并且部署文档里把JDK版本写清楚。

5.2 前后端联调时的经典问题:跨域

本地开发时前端在localhost:5173,后端在localhost:8080,两者不同端口,就会触发跨域。Nginx和XMLHttpRequest都会拦,但Spring Boot这边解决起来不复杂,写一个配置类:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意,加了allowCredentials(true)后,allowedOrigins不能再写"*",要用allowedOriginPatterns("*"),否则还是报跨域错误。这个小坑我花了一个下午才排查清楚,写在这里给你省时间。

如果你前端请求还带着自定义请求头,比如token,那一定要把allowedHeaders设成*,否则跨域预检请求会直接失败。

5.3 打包后静态资源404的排查思路

明明前端打包后dist内容放进static目录了,页面却打不开或CSS/JS404,我遇过几种原因:

  • 路径问题:Vue打包默认引用的是绝对路径/js/app.js,如果你把应用部署在子路径下(比如通过/greenhouse访问),就会404。解决办法是在Vue项目的vue.config.js里设publicPath: './'或者写相对路径。
  • 资源没有打进去:查看Jar包内容,用jar tf greenhouse-system.jar | grep static看看static里是否有文件。没有就说明没拷贝对位置。
  • 接口能通但页面白屏:打开浏览器控制台,如果报的是JS报错,那大概率是Vue编译后的资源引用了不存在的路径,优先检查publicPath和路由模式。

我每次交付前都会在干净的服务器上走一遍“解压Jar → 检查static → 启动 → 访问”,全流程验一遍再交付,避免用户拿到手一脸懵。

5.4 数据模拟与定时任务的坑

环境数据是模拟的,就得考虑数据的真实感。很多人直接一拍脑袋循环生成随机数,结果画出来的趋势图锯齿状,毫无规律,看着很假。我自己的做法是:生成数据时加上变化趋势,比如温度以24小时为周期,白天高、晚上低,再加一点随机扰动。公式大概是:

temp = 18 + 6 * sin((hour - 8) / 24 * 2 * PI) + random(-1, 1)

这样画出来的曲线才像真实的温度变化。演示的时候也会让人感觉系统“有智能感”,而不是假数据。

定时任务的另一个坑是:如果@Scheduled方法里直接操作数据库,一定要确保方法执行速度快,不要搞长事务。我曾经在定时任务里一边查数据一边调用外部接口,结果外部接口超时,定时任务线程被占满,整个应用其他请求都变慢了。后来我改成在定时任务里只做数据准备工作,把耗时操作扔到线程池异步执行,问题就消失了。

最后再说两句实在话

从拿到一个“基于Spring Boot的城郊蔬菜大棚管理与销售系统”的需求,到把源码、文档、部署全部交付,我最大的体会是:这类系统真正考验人的技术点不在某一个功能有多牛,而在你能否把一个“管理+销售”双闭环的流程整理清楚,并用代码稳定地跑起来。事务要不要加、库存怎么扣、环境数据怎么分组统计、部署时有哪些环境差异,任何一个环节含糊,后面都会给你颜色看。

我个人的建议是:拿到项目先画流程图,把“大棚—种植批次—商品—订单—库存”这条链路捋清楚,再动手建表和写代码。你会发现,只要这条数据流转主线是通的,所有功能模块都不会乱。后面如果你想扩展,还可以接真实的物联网传感器(比如通过MQTT上报温湿度)、接入微信小程序、把支付换成真实支付通道,这些都是在现有架构上做加法,不会推翻重来。

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

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

立即咨询