简介:这是一套面向高校计算机专业毕业设计的农产品商城与农资电商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源码与sql脚本,压缩包约97.29MB。目前已有119人学习,可作为毕设选题、功能拆解与代码实现的参考素材。
1. 农产品商城系统:从课程作业到能跑起来的农资电商 Java 项目
很多同学做毕业设计时,一看到“农产品商城系统”或“农资电商商城系统”就觉得无非是增删改查,结果真动手才发现,农资商品有季节性、有区域价、有起订量,订单还要区分自提和配送,库存扣减稍微不注意就超卖。这个标题对应的正是一套基于 Java 技术栈的 B2C/B2B 混合电商系统,核心是让农户或农资经销商能在线浏览、下单、支付、查看物流,后台能管商品、管订单、管用户。它适合计算机毕业设计选题,也适合刚学完 Spring Boot 想练一个完整业务闭环的 Java 新手。下面我按实际做项目的顺序,把选型、建表、核心接口、避坑点一次讲透,你照着能复现出一个可演示、可答辩的版本。
2. 技术选型与工程骨架:为什么用 Spring Boot + MyBatis 而不是 Servlet
2.1 农资电商场景对技术栈的真实要求
农资电商和普通商城最大的区别在于商品模型复杂:同一款化肥,不同品牌、不同规格、不同适用作物,价格和库存都不一样。这就要求后端能灵活处理多条件筛选和动态 SQL。如果还用原生 Servlet + JDBC,光是拼接查询条件就能写几百行,后期改一个字段要动好几个文件。所以常见做法是选 Spring Boot 做基础框架,MyBatis 做持久层,MySQL 存业务数据,Redis 缓存热点商品和会话。前端可以用 Vue 或 Thymeleaf,毕业设计里 Thymeleaf 更省事,不用单独部署前端工程。
选型理由很直接:Spring Boot 内嵌 Tomcat,打成 jar 就能跑,省去配置 web.xml 的麻烦;MyBatis 的 XML 映射文件对复杂查询友好,农资商品的多条件筛选用<if>标签就能动态拼 SQL;MySQL 免费且资料多,答辩时老师问底层也容易答。Java 版本建议用 8 或 11,别追最新,很多毕业设计用的依赖在 17 上会报模块化错误。Maven 管理依赖,IDEA 或 Eclipse 都能开发。
2.2 用 Spring Initializr 生成可运行的最小工程
第一步不是写代码,而是把工程骨架跑通。打开 start.spring.io,选 Maven、Java 8、Spring Boot 2.7.x,依赖勾选 Spring Web、MyBatis Framework、MySQL Driver、Lombok。生成后解压,用 IDEA 打开,等待依赖下载完成。然后改application.yml,配置数据库连接和 MyBatis 映射路径。
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/agri_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.agrimal.entity configuration: map-underscore-to-camel-case: true这段配置里,map-underscore-to-camel-case是关键,它让数据库的product_name自动映射到 Java 的productName,省去手写 resultMap。serverTimezone必须设成 Asia/Shanghai,否则插入时间会差 8 小时,答辩演示时订单时间对不上就尴尬了。数据库名agri_mall要先在 MySQL 里建好,字符集用 utf8mb4,支持 emoji 和生僻字。
2.3 分层包结构与统一返回格式
工程跑起来后,按controller、service、mapper、entity、dto、vo、common建包。common里放统一返回类Result<T>,包含 code、msg、data 三个字段。所有接口都返回这个格式,前端处理起来一致,也方便你加全局异常处理。比如商品列表接口返回Result.success(list),库存不足返回Result.error("库存不足")。这一步看着简单,但很多同学前期不统一,后期改接口格式改到崩溃,血泪经验就是一开始就定好。
3. 数据库设计:农资商品表和订单表的字段怎么定
3.1 商品表要预留农资特有字段
普通商城商品表可能只有名称、价格、库存、分类。农资商品必须加brand(品牌)、spec(规格,如 50kg/袋)、crop_type(适用作物)、region_limit(限售区域)、min_order_qty(起订量)。这些字段直接影响下单逻辑:起订量没达到不能提交订单,限售区域外的收货地址要拦截。建表 SQL 如下:
CREATE TABLE `product` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(128) NOT NULL COMMENT '商品名称', `brand` varchar(64) DEFAULT NULL COMMENT '品牌', `spec` varchar(64) DEFAULT NULL COMMENT '规格', `crop_type` varchar(64) DEFAULT NULL COMMENT '适用作物', `price` decimal(10,2) NOT NULL COMMENT '单价', `stock` int NOT NULL DEFAULT '0' COMMENT '库存', `min_order_qty` int DEFAULT '1' COMMENT '起订量', `region_limit` varchar(255) DEFAULT NULL COMMENT '限售区域,逗号分隔', `category_id` int DEFAULT NULL, `status` tinyint DEFAULT '1' COMMENT '1上架 0下架', PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;price用 decimal 不用 double,避免浮点精度问题,农资单价动辄几百块,差一分钱对不上账。region_limit存省份编码逗号分隔,简单够用,别一上来就搞区域表关联,毕业设计时间有限。status控制上下架,后台管理用。
3.2 订单表与订单明细表的关联设计
订单主表存订单号、用户 id、总金额、收货地址、订单状态、创建时间。订单明细表存订单 id、商品 id、购买数量、成交单价。注意成交单价要冗余存,不能只关联商品表查当前价,因为商品调价后历史订单金额会变,这是电商系统的基本要求。订单状态用枚举:0 待付款、1 已付款、2 已发货、3 已完成、4 已取消。
CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `user_id` bigint NOT NULL, `total_amount` decimal(10,2) NOT NULL, `address` varchar(255) NOT NULL, `status` tinyint DEFAULT '0', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL, `product_id` bigint NOT NULL, `product_name` varchar(128) NOT NULL, `price` decimal(10,2) NOT NULL, `quantity` int NOT NULL, PRIMARY KEY (`id`), KEY `idx_order` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;order_no加唯一索引,防止重复提交生成相同订单号。product_name也冗余存,商品被删除后订单详情还能显示。这些细节答辩时老师一问就能看出你有没有实际考虑过业务。
4. 核心业务接口:商品筛选、下单和库存扣减怎么写
4.1 多条件商品查询的 MyBatis 动态 SQL
农资商品筛选条件多,用户可能按品牌、作物、价格区间查。用 MyBatis 的<where>和<if>动态拼条件,避免写多个查询方法。
<select id="selectByCondition" resultType="com.example.agrimal.entity.Product"> SELECT * FROM product <where> <if test="brand != null and brand != ''"> AND brand = #{brand} </if> <if test="cropType != null and cropType != ''"> AND crop_type = #{cropType} </if> <if test="minPrice != null"> AND price >= #{minPrice} </if> <if test="maxPrice != null"> AND price <= #{maxPrice} </if> AND status = 1 </where> ORDER BY id DESC LIMIT #{offset}, #{limit} </select><where>标签会自动去掉第一个多余的 AND,不用手写 1=1。>和<是 XML 转义,直接写>和<会报错。分页用 LIMIT,offset 和 limit 由 Service 层算好传入。如果数据量大,可以换 PageHelper 插件,但毕业设计数据量小,手写分页更可控。
4.2 下单接口的库存扣减与事务控制
下单是核心,也是最容易翻车的地方。流程是:校验起订量、校验库存、扣库存、生成订单、生成明细。这五步必须在一个事务里,任何一步失败全部回滚。库存扣减用UPDATE product SET stock = stock - #{qty} WHERE id = #{id} AND stock >= #{qty},靠数据库行锁保证并发安全,返回影响行数为 0 说明库存不足。
@Service public class OrderService { @Autowired private ProductMapper productMapper; @Autowired private OrderMapper orderMapper; @Transactional(rollbackFor = Exception.class) public Result createOrder(Long userId, Long productId, int quantity, String address) { Product product = productMapper.selectById(productId); if (product == null || product.getStatus() == 0) { return Result.error("商品不存在或已下架"); } if (quantity < product.getMinOrderQty()) { return Result.error("未达到起订量:" + product.getMinOrderQty()); } int affected = productMapper.reduceStock(productId, quantity); if (affected == 0) { return Result.error("库存不足"); } Orders order = new Orders(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(product.getPrice().multiply(new BigDecimal(quantity))); order.setAddress(address); order.setStatus(0); orderMapper.insert(order); // 省略订单明细插入 return Result.success(order.getOrderNo()); } }@Transactional的rollbackFor = Exception.class必须加,否则遇到非运行时异常不回滚。reduceStock的 SQL 里带stock >= #{qty}条件,这是防超卖的关键,比先查再扣可靠得多。订单号生成可以用时间戳加随机数,别用 UUID,太长且无序,影响索引性能。
4.3 订单超时未支付自动取消的定时任务
用户下单后 30 分钟不付款,订单要自动取消并回滚库存。用 Spring 的@Scheduled定时任务,每分钟扫一次超时订单。
@Component public class OrderTimeoutTask { @Autowired private OrderMapper orderMapper; @Autowired private ProductMapper productMapper; @Scheduled(cron = "0 * * * * ?") public void cancelTimeoutOrders() { List<Orders> list = orderMapper.selectTimeoutOrders(30); for (Orders order : list) { orderMapper.updateStatus(order.getId(), 4); // 回滚库存,根据订单明细逐条加回 } } }cron = "0 * * * * ?"表示每分钟第 0 秒执行。selectTimeoutOrders查创建时间超过 30 分钟且状态为 0 的订单。回滚库存时要注意,如果商品已被删除,加回会失败,所以商品表最好用逻辑删除,别物理删除。这个定时任务在答辩时是个加分项,说明你考虑了完整业务闭环。
5. 避坑与排查:农资商城项目里最容易翻车的 4 个点
5.1 库存扣成负数
现象:并发下单后,商品库存显示 -5。原因:先查库存再扣减,两个线程同时查到库存 10,都以为够,各自扣 10。解决:用UPDATE ... WHERE stock >= qty原子操作,或者用 Redis 分布式锁。毕业设计用前者就够了,简单可靠。
5.2 订单金额与明细对不上
现象:订单主表总金额 100,明细加起来 120。原因:下单时先算总金额,再逐条插明细,中间商品调价了。解决:下单时把商品价格快照到明细里,总金额用明细累加,别单独算。或者加事务,但快照更稳妥。
5.3 中文乱码
现象:商品名称存进数据库变成问号。原因:数据库连接没设characterEncoding=utf8,或者建表时字符集不是 utf8mb4。解决:连接串加useUnicode=true&characterEncoding=utf8,建库建表统一 utf8mb4。已经乱码的数据改不回来,只能重插。
5.4 定时任务不执行
现象:超时订单一直不取消。原因:启动类没加@EnableScheduling。解决:在 Spring Boot 启动类上加这个注解。另外注意 cron 表达式是 6 位,不是 Linux 的 5 位,别写错。
6. 进阶技巧:用 Redis 缓存商品详情和验证接口性能
商品详情页是读多写少,每次查数据库浪费资源。用 Redis 缓存商品对象,key 用product:detail:{id},过期时间设 10 分钟。查的时候先查 Redis,没有再去数据库,然后回写。改商品时删缓存,别更新缓存,避免并发写不一致。
public Product getProductById(Long id) { String key = "product:detail:" + id; String json = redisTemplate.opsForValue().get(key); if (json != null) { return JSON.parseObject(json, Product.class); } Product product = productMapper.selectById(id); if (product != null) { redisTemplate.opsForValue().set(key, JSON.toJSONString(product), 10, TimeUnit.MINUTES); } return product; }验证性能可以用 JMeter 或 Apache Bench,对商品列表接口压 100 并发,看响应时间和错误率。没加缓存前可能 200ms,加了之后降到 20ms 以内。答辩时拿这个数据说话,比空口说“性能好”有说服力。我一般还会在接口里加个简单的耗时日志,用System.currentTimeMillis()前后差,方便定位慢查询。
做这个项目最大的教训是:别一上来就堆功能,先把商品、订单、库存三个核心表跑通,再逐步加支付、物流、评论。我见过太多同学前期贪多,最后连下单都跑不起来。希望帮到你。
本文还有配套的精品资源,点击获取