简介:本资源为基于微信小程序的云浮市特色农产品交易系统毕业设计论文,面向计算机相关专业本科生及需要完成类似选题的开发者。论文围绕传统农产品线下销售渠道受限、信息不对称等痛点,采用Java语言结合Spring Boot框架与MySQL数据库,设计并实现了一套集农产品浏览、购物车、下单支付、订单与物流查询于一体的交易平台,商家端则提供商品管理、订单处理与用户数据查看等后台功能。压缩包内共1个docx文件,约1.13MB,内容涵盖摘要、目录、开发工具与技术介绍、系统设计与实现等完整章节,结构规范,可直接作为毕业设计写作模板与项目开发参考。目前已有51人学习下载,适合需要借鉴技术选型、功能模块划分与论文撰写思路的读者参考使用。
1. 从一份毕业设计标题说起:农产品交易小程序到底在解决什么问题
去年帮几个做毕业设计的学生看代码,十份里有七份标题里带着“微信小程序”和“特色农产品交易”。这个组合不是偶然——它踩中了三个真实需求:产地信息不对称、中间环节吃掉利润、消费者想买“有来头”的东西却找不到可信渠道。云浮市这个限定词把范围收窄到粤西山区,那里有砂糖橘、荔枝、腐竹、南药等品类,但农户分散、物流成本高、品牌弱,传统电商平台抽成又重。一个基于 Spring Boot 的微信小程序交易系统,核心价值不是“又一个商城”,而是把产地直供的链路缩短到“农户上架—消费者下单—产地发货”三步,同时用微信的社交关系链做信任背书。适合谁做?计算机专业毕业设计选题、想练手全栈开发的学生、以及真正想给地方农产品做轻量级数字化工具的小团队。这一章不聊虚的,先把“为什么是 Spring Boot + 微信小程序”这个选型逻辑讲透,再往下拆实现。
2. 技术选型与架构拆解:为什么是 Spring Boot 加微信小程序
2.1 后端为什么选 Spring Boot 而不是别的
农产品交易系统的后端需求很明确:商品管理、订单状态机、微信支付回调、库存扣减、用户会话。这些需求用 Spring Boot 做,最大的好处是生态成熟——MyBatis-Plus 处理 CRUD、Redis 做库存预扣和会话缓存、RabbitMQ 或 RocketMQ 做订单超时取消,都有现成方案。另一个现实原因是毕业设计场景下,Spring Boot 的文档和社区案例最多,遇到问题能搜到答案。如果换成 Go 或 Node.js,性能可能更好,但调试成本和答辩时老师能看懂的概率会下降。我一般建议学生用 Spring Boot 2.7.x 或 3.x,JDK 选 8 或 17,数据库 MySQL 8.0,缓存 Redis 6+。版本不要追最新,稳定优先。
2.2 微信小程序端的技术边界
小程序端不是“随便写写页面”那么简单。农产品交易有几个特殊点:图片多且大(产地实拍、果园视频封面)、列表页需要下拉刷新和分页、支付流程必须走wx.requestPayment、收货地址要用wx.chooseAddress。小程序的包体积限制是 2MB(主包),所以图片必须走 CDN 或云存储,不能打包进代码。常用做法是:前端用原生小程序或 uni-app,状态管理用 MobX 或简单的全局变量,请求封装一层request.js统一处理 token 和错误码。注意,小程序不能直接连数据库,所有数据交互必须经过后端 API,所以后端要设计好 RESTful 接口或 GraphQL。
2.3 整体架构与数据流
一个可落地的架构是这样的:小程序端发起请求 → Nginx 反向代理 → Spring Boot 应用 → MySQL 主从(或单机)→ Redis 缓存 → 微信支付回调 → 消息队列处理异步任务。数据流上,用户下单时先扣 Redis 库存,再写订单表,支付成功后更新订单状态并通知发货。如果支付超时,定时任务或延迟队列把库存回滚。这个链路里最容易出问题的是“库存扣减”和“支付回调幂等”,后面避坑章节会细说。
| 层级 | 技术选型 | 作用 |
|---|---|---|
| 前端 | 微信小程序原生 / uni-app | 用户交互、支付调起 |
| 网关 | Nginx | 反向代理、静态资源 |
| 后端 | Spring Boot + MyBatis-Plus | 业务逻辑、API |
| 数据库 | MySQL 8.0 | 持久化存储 |
| 缓存 | Redis 6+ | 库存、会话、热点数据 |
| 消息 | RabbitMQ / RocketMQ | 异步解耦、延迟任务 |
| 存储 | 对象存储(如 MinIO 或云 OSS) | 商品图片、视频 |
3. 从零搭建后端:商品、订单、支付三个核心模块的代码实现
3.1 商品模块:表设计与分页查询接口
商品表product至少包含:id、title、cover_url、price、stock、category_id、origin(产地)、status(上架/下架)、create_time。产地字段是特色农产品的关键,建议单独建region表或用枚举。分页查询用 MyBatis-Plus 的Page对象,配合条件构造器。
// ProductController.java @RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/list") public Result<Page<ProductVO>> list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) String keyword, @RequestParam(required = false) Long categoryId) { // 构建分页对象,注意页码从1开始 Page<Product> pageObj = new Page<>(page, size); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Product::getStatus, 1); // 只查上架商品 if (StringUtils.hasText(keyword)) { wrapper.like(Product::getTitle, keyword); } if (categoryId != null) { wrapper.eq(Product::getCategoryId, categoryId); } wrapper.orderByDesc(Product::getCreateTime); Page<Product> result = productService.page(pageObj, wrapper); // 转换为VO,隐藏敏感字段 return Result.success(result.convert(ProductVO::fromEntity)); } }逻辑说明:Page对象由 MyBatis-Plus 提供,page和size参数控制分页。LambdaQueryWrapper避免硬编码字段名,eq和like是常用条件。convert方法把实体转成 VO,防止把库存等内部字段暴露给前端。参数上,page默认 1,size默认 10,实际项目中建议限制size最大值(比如 50),防止恶意请求拖垮数据库。
3.2 订单模块:状态机与库存扣减
订单状态建议用枚举:待支付、已支付、已发货、已完成、已取消。状态流转必须单向,不能从“已完成”跳回“待支付”。库存扣减有两种常见做法:下单减库存和支付减库存。农产品有季节性,超卖风险高,我一般用“下单预扣 + 支付确认 + 超时回滚”。Redis 的decrement是原子操作,适合做预扣。
// OrderService.java 片段 public Order createOrder(Long userId, Long productId, Integer quantity) { String stockKey = "stock:product:" + productId; // 先检查库存是否足够 String stockStr = redisTemplate.opsForValue().get(stockKey); if (stockStr == null) { // 缓存未命中,从数据库加载并回填 Product product = productMapper.selectById(productId); redisTemplate.opsForValue().set(stockKey, String.valueOf(product.getStock())); stockStr = String.valueOf(product.getStock()); } int stock = Integer.parseInt(stockStr); if (stock < quantity) { throw new BusinessException("库存不足"); } // 原子扣减 Long remain = redisTemplate.opsForValue().decrement(stockKey, quantity); if (remain < 0) { // 扣减后为负,说明并发下超卖,回滚 redisTemplate.opsForValue().increment(stockKey, quantity); throw new BusinessException("库存不足"); } // 写订单 Order order = new Order(); order.setUserId(userId); order.setProductId(productId); order.setQuantity(quantity); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); order.setCreateTime(new Date()); orderMapper.insert(order); // 发送延迟消息,30分钟未支付则取消 rabbitTemplate.convertAndSend("order.delay.exchange", "order.delay", order.getId()); return order; }逻辑说明:先读 Redis 库存,未命中则从数据库加载。decrement返回扣减后的值,如果小于 0 说明并发下超卖,立即increment回滚。订单写入后发延迟消息,30 分钟后检查支付状态。参数上,quantity要校验正整数,productId要校验存在且上架。注意,Redis 库存和数据库库存最终要一致,通常用定时任务对账。
3.3 支付模块:微信支付回调与幂等处理
微信支付回调是整个系统最容易翻车的地方。回调可能重复发送,所以必须做幂等。常见做法是:回调时先查订单状态,如果已经是“已支付”就直接返回成功,不再处理。另外,回调的签名验证不能省,否则会被伪造请求。
// WxPayCallbackController.java @PostMapping("/callback") public String payCallback(@RequestBody String body, HttpServletRequest request) { // 验证签名,具体工具类省略 if (!wxPayService.verifySign(request)) { return "FAIL"; } JSONObject json = JSON.parseObject(body); String outTradeNo = json.getString("out_trade_no"); String transactionId = json.getString("transaction_id"); // 幂等:查订单状态 Order order = orderMapper.selectByOrderNo(outTradeNo); if (order == null) { return "FAIL"; } if (OrderStatus.PAID.getCode().equals(order.getStatus())) { return "SUCCESS"; // 已处理过,直接返回成功 } // 更新订单状态 order.setStatus(OrderStatus.PAID.getCode()); order.setTransactionId(transactionId); order.setPayTime(new Date()); orderMapper.updateById(order); // 扣减数据库库存(最终一致) productMapper.decreaseStock(order.getProductId(), order.getQuantity()); return "SUCCESS"; }逻辑说明:先验签,再解析out_trade_no。幂等判断放在更新之前,避免重复扣库存。返回SUCCESS告诉微信不再重试。参数上,out_trade_no是商户订单号,必须全局唯一。注意,数据库库存扣减要用update product set stock = stock - #{quantity} where id = #{id} and stock >= #{quantity},防止负库存。
4. 小程序端关键页面与交互:从商品列表到支付调起
4.1 商品列表与详情页的数据绑定
小程序端用wx.request调后端接口,列表页用scroll-view或onReachBottom做分页。商品卡片要展示封面、标题、价格、产地。详情页需要轮播图、规格选择、加入购物车或直接购买。注意,小程序的image组件要设置mode="aspectFill"防止图片变形,图片 URL 必须用 HTTPS。
// pages/product/list.js Page({ data: { products: [], page: 1, size: 10, hasMore: true }, onLoad() { this.loadProducts(); }, loadProducts() { if (!this.data.hasMore) return; wx.request({ url: 'https://your-domain.com/api/product/list', data: { page: this.data.page, size: this.data.size }, success: (res) => { if (res.data.code === 200) { const list = res.data.data.records; this.setData({ products: this.data.products.concat(list), page: this.data.page + 1, hasMore: list.length === this.data.size }); } } }); }, onReachBottom() { this.loadProducts(); } });逻辑说明:onReachBottom是小程序自带的下拉触底事件,触发时加载下一页。hasMore根据返回条数判断是否还有数据。参数上,page和size要和后端约定一致。注意,wx.request的域名必须在小程序后台配置白名单,否则请求会被拦截。
4.2 支付流程:从下单到 wx.requestPayment
支付流程分三步:调后端创建订单 → 后端返回支付参数 → 小程序调wx.requestPayment。支付参数包括timeStamp、nonceStr、package、signType、paySign,这些都由后端生成,前端不能自己拼。
// pages/order/confirm.js submitOrder() { wx.request({ url: 'https://your-domain.com/api/order/create', method: 'POST', data: { productId: this.data.productId, quantity: this.data.quantity }, success: (res) => { if (res.data.code === 200) { const payParams = res.data.data.payParams; wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: payParams.signType, paySign: payParams.paySign, success: () => { wx.showToast({ title: '支付成功' }); wx.redirectTo({ url: '/pages/order/list' }); }, fail: () => { wx.showToast({ title: '支付取消', icon: 'none' }); } }); } } }); }逻辑说明:wx.requestPayment的package参数格式是prepay_id=xxx,由后端调微信统一下单接口获得。paySign是后端用商户私钥签名的,前端只负责传参。注意,支付成功后要跳转到订单列表,不能停留在原页面,否则用户可能重复支付。
5. 避坑与排查:库存超卖、支付回调、图片加载的翻车现场
5.1 库存超卖:Redis 扣减后数据库没同步
现象:并发下单时,Redis 库存扣到 0,但数据库库存还是正数,导致超卖。原因:Redis 扣减和数据库扣减是两步,中间可能失败。解决:用延迟队列或定时任务对账,把 Redis 库存和数据库库存做比较,不一致时以数据库为准回滚 Redis。更稳妥的做法是数据库扣减用stock >= quantity条件,扣减失败则回滚 Redis。
5.2 支付回调重复:订单被扣两次库存
现象:微信回调重复发送,订单状态被更新两次,库存扣了两次。原因:没有做幂等判断。解决:回调入口先查订单状态,如果已经是“已支付”直接返回SUCCESS。另外,数据库库存扣减要加stock >= quantity条件,防止负库存。
5.3 图片加载失败:小程序域名白名单没配
现象:商品图片显示空白,控制台报“不在以下 request 合法域名列表中”。原因:图片 URL 的域名没有在小程序后台配置。解决:登录小程序后台,在“开发设置”里添加图片域名和接口域名。注意,域名必须备案且支持 HTTPS。
5.4 订单超时未取消:延迟消息没生效
现象:用户下单后 30 分钟未支付,订单还是“待支付”,库存被占用。原因:延迟消息没发出去,或者消费者没处理。解决:检查 RabbitMQ 的延迟插件是否安装,或者用定时任务扫表。定时任务建议每分钟跑一次,查create_time超过 30 分钟且状态为“待支付”的订单,批量取消并回滚库存。
5.5 小程序包体积超限:图片没走 CDN
现象:上传代码时提示“主包超过 2MB”。原因:图片打包进了代码目录。解决:把图片上传到对象存储,代码里只存 URL。另外,可以用小程序的subpackages做分包,把不常用的页面放到子包。
6. 进阶技巧:用对账任务和接口幂等把系统做稳
6.1 对账任务:每天凌晨跑一次库存和订单核对
对账任务的核心是“以数据库为准,修正 Redis”。具体做法:每天凌晨 2 点,查所有上架商品的数据库库存,和 Redis 库存比较,不一致的以数据库为准写回 Redis。同时,查所有“待支付”超过 30 分钟的订单,批量取消并回滚库存。这个任务用 Spring 的@Scheduled注解就能实现。
@Component public class ReconcileTask { @Autowired private ProductMapper productMapper; @Autowired private RedisTemplate<String, String> redisTemplate; @Autowired private OrderMapper orderMapper; @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点 public void reconcileStock() { List<Product> products = productMapper.selectList(null); for (Product product : products) { String key = "stock:product:" + product.getId(); String redisStock = redisTemplate.opsForValue().get(key); if (redisStock == null || !redisStock.equals(String.valueOf(product.getStock()))) { redisTemplate.opsForValue().set(key, String.valueOf(product.getStock())); } } // 取消超时订单 List<Order> timeoutOrders = orderMapper.selectTimeoutOrders(30); for (Order order : timeoutOrders) { order.setStatus(OrderStatus.CANCELLED.getCode()); orderMapper.updateById(order); // 回滚Redis库存 redisTemplate.opsForValue().increment("stock:product:" + order.getProductId(), order.getQuantity()); } } }逻辑说明:@Scheduled的 cron 表达式0 0 2 * * ?表示每天凌晨 2 点执行。先对账库存,再处理超时订单。参数上,超时时间 30 分钟可以根据业务调整。注意,对账任务要加日志,方便排查。
6.2 接口幂等:用 token 或唯一索引防重复提交
除了支付回调,创建订单接口也要做幂等。常见做法是前端传一个requestId,后端用 Redis 的setnx判断是否已处理。或者用数据库唯一索引,比如order_no唯一,重复插入会报错。我一般用 Redis 的setIfAbsent,key 是order:request:{requestId},过期时间 5 分钟。
public Order createOrderWithIdempotent(Long userId, Long productId, Integer quantity, String requestId) { String key = "order:request:" + requestId; Boolean success = redisTemplate.opsForValue().setIfAbsent(key, "1", 5, TimeUnit.MINUTES); if (Boolean.FALSE.equals(success)) { throw new BusinessException("请勿重复提交"); } try { return createOrder(userId, productId, quantity); } catch (Exception e) { redisTemplate.delete(key); // 失败时删除,允许重试 throw e; } }逻辑说明:setIfAbsent是原子操作,只有 key 不存在时才设置成功。过期时间 5 分钟防止死锁。失败时删除 key,让用户可以重试。参数上,requestId由前端生成,通常是 UUID。
6.3 日志与监控:用 AOP 记录关键操作
农产品交易系统涉及钱,日志不能少。用 Spring AOP 切@Log注解,记录操作人、操作类型、参数、结果。日志写到文件或数据库,方便对账和排查。监控可以用 Spring Boot Actuator + Prometheus,看接口 QPS、响应时间、错误率。
@Aspect @Component public class LogAspect { @Around("@annotation(logAnnotation)") public Object around(ProceedingJoinPoint joinPoint, Log logAnnotation) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - start; // 记录日志,省略具体实现 System.out.println("方法:" + joinPoint.getSignature().getName() + ",耗时:" + cost + "ms"); return result; } }逻辑说明:@Around环绕通知,在方法执行前后记录时间。@annotation(logAnnotation)匹配带@Log注解的方法。参数上,cost是耗时,超过阈值可以告警。注意,日志不要记录敏感信息,比如密码、支付签名。
做这类系统,我最大的教训是:不要相信“一次就能跑通”。库存、支付、回调这三个地方,每个都至少踩过两次坑。后来我养成了一个习惯:任何涉及钱和库存的操作,先写对账任务,再写业务逻辑。对账任务就像后悔药,出了问题能兜底。希望帮到你。
本文还有配套的精品资源,点击获取