简介:这是一套面向高校计算机专业毕业设计的农产品商城与农资电商Java Web项目源码,适合正在准备毕设、需要完整可运行参考案例的本科或专科学生。项目基于Struts、JSP、JDBC、Spring技术栈开发,运行环境为Java 1.8、MySQL 5.7以上与Tomcat 8.5,可在IDEA或Eclipse中导入使用。前台覆盖会员注册登录、商品浏览、购物车、收藏、下单支付、公告查看、留言及个人信息管理;后台包含类别、商品、会员、订单、退货、库存记录与盘点、销售统计、财务统计、系统公告、留言板及密码修改等模块,业务链路较为完整。压缩包共2004个文件,以js脚本、gif与png图片、html页面、jar依赖、jsp页面、class字节码、css样式、xml配置及java源码为主,整体约97.29MB,目录结构清晰,便于按模块查阅与二次修改。目前已有119人学习,可作为毕设选题参考、功能拆解与代码排错的实用素材。
1. 农产品商城系统到底在做什么:从农资电商的真实业务说起
很多同学拿到「农产品商城系统」这个毕业设计题目,第一反应是「不就是个卖东西的网站吗」,然后照着网上那种通用商城模板改改商品名就交了。答辩的时候老师问一句「你这个和普通电商有啥区别」,当场就卡壳。农产品和农资电商的业务逻辑跟普通商城差别很大:农产品有产地、季节、保质期、批次的概念,农资商品(种子、化肥、农药)有登记证号、适用作物、施用规范,甚至还有区域限购。这些才是这个题目真正要你体现的东西。
这个系统本质上是一个基于 Java 技术栈的 B2C/B2B2C 电商平台,核心要解决的是「农户/农资店卖货 → 平台管理 → 买家下单 → 订单履约」这条链路。它适合计算机相关专业做毕业设计的同学,也适合想练手 Spring Boot 全栈开发的人。下面我按实际能跑起来的思路,把技术选型、数据库设计、核心模块实现和踩坑点一层层拆开讲,你照着做能出一个能答辩、能演示、代码量够的完整项目。
2. 技术选型与工程骨架:为什么用 Spring Boot + MyBatis 而不是别的
2.1 后端框架选型:Spring Boot 是毕业设计的最优解
毕业设计的时间通常只有两三个月,你不可能从零搭一套框架。Spring Boot 的优势在于自动配置和起步依赖,一个spring-boot-starter-web就能把 Tomcat、Spring MVC、Jackson 全部拉进来,省掉大量 XML 配置。对比 SSM(Spring + Spring MVC + MyBatis)那种需要手动配web.xml、applicationContext.xml的老方案,Spring Boot 至少能帮你省一周的调试时间。
版本上我一般选 Spring Boot 2.7.x,这个版本稳定、资料多、和 JDK 8/11 兼容性好。不要盲目上 Spring Boot 3.x,它要求 JDK 17,很多学校的实验环境还停留在 JDK 8,到时候跑不起来反而麻烦。持久层用 MyBatis-Plus,它在 MyBatis 基础上封装了通用 CRUD,单表查询基本不用写 SQL,分页插件也是现成的。前端如果不想搞太复杂,用 Thymeleaf 做服务端渲染就够了;想显得现代一点就用 Vue + Element UI 前后端分离,但要注意跨域和登录态的问题。
数据库选 MySQL 8.0,字符集用utf8mb4,因为农产品名称里可能有生僻字或者 emoji。缓存用 Redis 存 session 和热点商品,文件存储本地或者用 MinIO 都行,毕业设计用本地磁盘就够了。
2.2 工程目录结构与依赖配置
一个清晰的目录结构能让答辩老师一眼看出你的工程素养。我一般按功能模块分包,而不是按技术分层分包:
src/main/java/com/example/agrimall/ ├── common/ # 通用工具、统一返回结果、异常处理 ├── config/ # 配置类:Redis、MyBatis-Plus、拦截器 ├── controller/ # 接口层 ├── service/ # 业务逻辑 │ └── impl/ ├── mapper/ # MyBatis Mapper 接口 ├── entity/ # 数据库实体 ├── dto/ # 数据传输对象 └── vo/ # 视图返回对象pom.xml里核心依赖如下,注意版本要对应:
<dependencies> <!-- Web 起步依赖,内含 Tomcat 和 Spring MVC --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus,简化单表 CRUD --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <!-- Redis 做缓存和 session 共享 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!-- Lombok 减少 getter/setter 样板代码 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这段配置的逻辑是:Web 依赖提供接口能力,MyBatis-Plus 负责数据库操作,Redis 处理缓存,Lombok 简化实体类。参数上唯一要注意的是 MyBatis-Plus 版本要和 Spring Boot 版本匹配,3.5.x 对应 Spring Boot 2.x,如果你用 Spring Boot 3.x 需要换 3.5.4 以上。application.yml里配好数据源和 Redis 连接,mapper-locations指向classpath*:mapper/*.xml,否则 XML 映射文件加载不到,这是新手最常见的翻车点。
2.3 数据库表设计:农产品和农资的特殊字段
普通商城的商品表只有名称、价格、库存,但农产品商城必须加这些字段:
| 字段名 | 类型 | 说明 |
|---|---|---|
| origin | varchar(100) | 产地,如「山东寿光」 |
| harvest_date | date | 采收日期,用于计算新鲜度 |
| shelf_life | int | 保质期天数 |
| batch_no | varchar(50) | 批次号,农资追溯用 |
| registration_no | varchar(50) | 农药/化肥登记证号 |
| applicable_crop | varchar(200) | 适用作物,农资专用 |
订单表要加delivery_type(自提/快递/冷链)和expected_delivery(期望送达时间),因为农产品对时效要求高。用户表要区分角色:普通买家、农户卖家、农资经销商、平台管理员,用role字段控制权限。这些字段不是凑数,答辩时老师一看就知道你理解业务。
3. 核心模块实现:商品、订单、购物车怎么落地
3.1 商品模块:分类树与 SKU 设计
农产品分类通常是三级:一级「蔬菜」→ 二级「叶菜类」→ 三级「菠菜」。用parent_id做自关联,查询时递归或者用路径字段path(如1/5/12)加速。SKU 设计上,农产品一般按重量卖(500g、2.5kg、5kg),农资按规格卖(袋、瓶、箱),所以 SKU 表要有spec_name和spec_value两个字段。
商品列表接口用 MyBatis-Plus 的分页插件:
// 分页查询商品,支持按分类和关键词过滤 @GetMapping("/list") public Result<Page<ProductVO>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { Page<Product> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); // 分类过滤,注意 categoryId 为空时不加条件 wrapper.eq(categoryId != null, Product::getCategoryId, categoryId); // 关键词模糊匹配商品名和产地 wrapper.and(StringUtils.hasText(keyword), w -> w .like(Product::getName, keyword) .or() .like(Product::getOrigin, keyword)); wrapper.eq(Product::getStatus, 1); // 只查上架商品 wrapper.orderByDesc(Product::getCreateTime); Page<Product> result = productService.page(page, wrapper); // 转 VO,补充分类名称等字段 return Result.success(convertToVO(result)); }逻辑说明:LambdaQueryWrapper用方法引用代替字符串字段名,避免写错列名。eq和and的第一个参数是条件布尔值,只有为 true 时才拼接 SQL,这样空参数不会污染查询。参数上pageNum和pageSize要做上限控制,比如pageSize最大 100,防止有人传 10000 把数据库拖垮。返回的 VO 里要带分类名称和主图 URL,前端不用再发请求。
3.2 订单模块:下单、库存扣减与超时取消
订单是电商系统最容易出 bug 的地方。下单流程要在一个事务里完成:校验库存 → 扣减库存 → 生成订单 → 生成订单明细 → 清空购物车。库存扣减必须用乐观锁或者update ... where stock >= #{num}的方式,否则并发下会超卖。
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId, OrderCreateDTO dto) { // 1. 校验商品和库存 List<OrderItem> items = new ArrayList<>(); BigDecimal totalAmount = BigDecimal.ZERO; for (OrderItemDTO itemDTO : dto.getItems()) { Product product = productMapper.selectById(itemDTO.getProductId()); if (product == null || product.getStatus() != 1) { throw new BizException("商品不存在或已下架"); } // 2. 扣减库存,用 where stock >= num 防止超卖 int affected = productMapper.deductStock( itemDTO.getProductId(), itemDTO.getQuantity()); if (affected == 0) { throw new BizException("库存不足:" + product.getName()); } // 3. 组装订单明细,价格以数据库为准,不信任前端传的价格 OrderItem item = new OrderItem(); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setPrice(product.getPrice()); item.setQuantity(itemDTO.getQuantity()); items.add(item); totalAmount = totalAmount.add( product.getPrice().multiply(BigDecimal.valueOf(itemDTO.getQuantity()))); } // 4. 生成订单主表 Order order = new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(OrderStatus.UNPAID.getCode()); orderMapper.insert(order); // 5. 批量插入明细 items.forEach(i -> { i.setOrderId(order.getId()); orderItemMapper.insert(i); }); // 6. 清空已下单的购物车项 cartMapper.deleteByUserIdAndProductIds(userId, dto.getProductIds()); return convertToVO(order); }关键点:deductStock的 SQL 是update product set stock = stock - #{num} where id = #{id} and stock >= #{num},靠数据库行锁保证原子性。订单号生成用「时间戳 + 用户 ID 后四位 + 随机数」,不要用 UUID,太长且无序。超时取消用定时任务,每 5 分钟扫一次status = 未支付 and create_time < now() - 30分钟的订单,取消并回滚库存。这里要注意回滚库存也要用stock = stock + num,并且记录库存流水,方便对账。
3.3 购物车与 Redis 缓存
购物车用 Redis Hash 存储,key 是cart:{userId},field 是productId,value 是数量。这样读写快,还能设过期时间。加购时先查商品是否上架,再hIncrBy增加数量。查询购物车时用hGetAll拿到所有商品 ID,再批量查商品信息组装返回。
// 加入购物车 public void addToCart(Long userId, Long productId, Integer quantity) { Product product = productMapper.selectById(productId); if (product == null || product.getStatus() != 1) { throw new BizException("商品不可购买"); } String key = "cart:" + userId; redisTemplate.opsForHash().increment(key, productId.toString(), quantity); // 设置 7 天过期,避免 Redis 无限增长 redisTemplate.expire(key, 7, TimeUnit.DAYS); }参数说明:increment是原子操作,并发加购不会丢数据。过期时间设 7 天是折中,太短用户下次来购物车空了体验差,太长占内存。热点商品信息也放 Redis,key 是product:info:{id},过期时间 10 分钟,更新商品时主动删除缓存,避免脏读。
4. 避坑与排查:毕业设计里最容易翻车的 5 个地方
4.1 中文乱码:从数据库到前端全链路排查
现象:商品名称在数据库里是好的,但接口返回给前端变成????或者乱码。原因通常是数据库连接 URL 没加字符集参数,或者 Tomcat 的URIEncoding没配。解决:JDBC URL 加上useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai,数据库和表的字符集统一用utf8mb4,Spring Boot 的server.servlet.encoding.charset=UTF-8和force=true也要配上。前端请求头Content-Type带charset=UTF-8。
4.2 跨域问题:前后端分离时的 CORS 配置
现象:前端 Vue 跑在 8080,后端跑在 8081,浏览器控制台报Access-Control-Allow-Origin。原因:浏览器同源策略拦截。解决:后端加全局 CORS 配置,实现WebMvcConfigurer的addCorsMappings,允许前端域名、方法和请求头。注意allowCredentials(true)时allowedOrigins不能用*,要写具体域名,否则浏览器会拒绝。
4.3 事务失效:为什么库存扣了订单没生成
现象:下单时库存扣了,但订单表没数据,或者反过来。原因:@Transactional方法被同类内部调用,代理不生效;或者异常被 catch 了没抛出去,事务不回滚。解决:确保事务方法通过 Spring 代理调用,异常要throw出去,rollbackFor设成Exception.class。另外注意 MySQL 表引擎必须是 InnoDB,MyISAM 不支持事务。
4.4 图片上传失败:路径和大小限制
现象:上传商品图片报 500 或者文件为空。原因:Spring Boot 默认上传大小限制是 1MB,农产品图片往往超过;或者保存路径不存在。解决:application.yml里配spring.servlet.multipart.max-file-size=10MB和max-request-size=10MB,保存前用File.mkdirs()创建目录,文件名用 UUID 重命名避免中文和重复。
4.5 定时任务不执行:超时订单一直挂着
现象:配置了超时取消订单的定时任务,但订单一直不取消。原因:忘了在启动类加@EnableScheduling,或者 cron 表达式写错。解决:启动类加注解,cron 用在线工具验证,比如0 */5 * * * ?表示每 5 分钟执行一次。另外定时任务方法不能有参数,否则不执行。
5. 进阶技巧:用状态机管订单,用压测验证并发
订单状态流转是毕业设计里能体现深度的点。不要用一堆 if-else 判断状态,用状态机模式:定义OrderStatus枚举和允许的流转路径,比如「待支付 → 已支付 → 已发货 → 已完成」,非法流转直接抛异常。这样代码清晰,答辩时也能讲出设计模式的应用。
public enum OrderStatus { UNPAID(0, "待支付"), PAID(1, "已支付"), SHIPPED(2, "已发货"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); // 定义每个状态能流转到哪些状态 private static final Map<OrderStatus, Set<OrderStatus>> TRANSITIONS = Map.of( UNPAID, Set.of(PAID, CANCELLED), PAID, Set.of(SHIPPED, CANCELLED), SHIPPED, Set.of(COMPLETED), COMPLETED, Set.of(), CANCELLED, Set.of() ); public static void checkTransition(OrderStatus from, OrderStatus to) { if (!TRANSITIONS.get(from).contains(to)) { throw new BizException("非法状态流转:" + from.desc + " → " + to.desc); } } }验证并发是否真的安全,用 JMeter 或者 Apache Bench 压测下单接口。命令ab -n 1000 -c 100 -p order.json -T application/json http://localhost:8080/order/create,看库存有没有变成负数。如果超卖,检查扣减 SQL 是不是用了stock >= num条件。压测前把日志级别调到 WARN,否则日志 IO 会拖慢结果。
我自己的习惯是每写完一个模块就用 Postman 跑一遍正向和逆向用例,逆向用例包括库存不足、商品下架、重复下单、非法状态流转。这些用例留着,答辩演示时随机抽一个跑,比干讲代码有说服力。希望帮到你。
本文还有配套的精品资源,点击获取