接手过不少这种项目一听到“基于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/** 开头需要role0 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, statusgreenhouse大棚信息表greenhouse_code, name, area, location, type, statussensor_data环境数据表greenhouse_id, temperature, humidity, light, soil_humidity, co2, status, create_timeplant_batch种植批次表greenhouse_id, crop_name, seed_time, plan_harvest_time, actual_harvest_time, statusfarm_task农事任务表batch_id, task_type, description, plan_time, status, operator_idproduct商品表name, category, price, stock, unit, image, description, statuscart购物车表user_id, product_id, quantityorder订单表order_no, user_id, total_amount, status, address, create_timeorder_item订单明细表order_id, product_id, quantity, pricenotice消息通知表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的代码量能降低一半以上。实体类直接继承BaseMapperData 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 BaseMapperGreenhouse { // 增删改查、分页查询全继承了 }Service层里条件查询用LambdaQueryWrapper非常顺手比如查某个大棚最新一条环境数据// 查2号大棚最新一条环境记录 SensorData sensorData sensorDataMapper.selectOne( new LambdaQueryWrapperSensorData() .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连接串上一定要加serverTimezoneAsia/Shanghai否则你本地看数据是准的上了服务器就发现数据线整体挪了一截特别容易误判成代码逻辑错了。这个统计SQL在MyBatis-Plus里用自定义Mapper方法写比较自然如果你不想写XML也可以用Select注解写在Mapper接口里。接口层返回的是ListMapString, 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.activeprod不用改代码。打包执行mvn clean package -DskipTests打包成功后在target目录下会生成一个xxx.jar包。我每次交付前都会验证两个事第一是不是能直接java -jar跑起来不依赖IDE第二启动时日志里有没有明显报错比如端口占用、数据库连接失败。这两个验证通过说明打包没问题。4.2 Linux服务器部署实操服务器上部署我的标准操作流程是这样的上传Jar包用scp、rsync或者直接在服务器上拉取。注意上传后赋予执行权限chmod x greenhouse-system.jar启动先用前台模式启动一次确认能正常起来java -jar greenhouse-system.jar --spring.profiles.activeprod看到Started Application in X seconds再按CtrlC停掉然后改用后台运行nohup java -jar greenhouse-system.jar --spring.profiles.activeprod app.log 21 检查状态jps命令看Java进程在不在tail -f app.log看日志输出curl http://localhost:8080/api/health看接口是否响应。配置Nginx如果前端是Vue单独部署Nginx做静态文件服务和API反向代理。这里有个老生常谈但容易配错的点location /api/要设置proxy_pass http://127.0.0.1:8080;并且注意结尾的斜杠问题少一个斜杠会把/api前缀也带过去了。用systemd管理服务呢这个是生产环境更正规的做法。在/etc/systemd/system/greenhouse.service里写一个Unit文件[Unit] DescriptionGreenhouse System Afternetwork.target [Service] Typesimple Userroot ExecStart/usr/bin/java -jar /opt/greenhouse/greenhouse-system.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-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.port8081。数据库连接不上报错信息里有Communications link failure或Access denied for user。前者查IP、端口、useSSLfalse后者查账号密码、数据库授权。我在部署文档里会特别提醒MySQL 8.0要用com.mysql.cj.jdbc.Driverdriver-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上报温湿度、接入微信小程序、把支付换成真实支付通道这些都是在现有架构上做加法不会推翻重来。
阅读完成 · 觉得有帮助?