简介:这是一套面向计算机专业本科生的毕业设计级实战项目,基于微信小程序与Java后端构建农产品自主供销系统,适用于课程设计、毕设选题及全栈开发能力训练。资源完整覆盖前后端协同开发全流程:前端采用微信原生框架(wxml/wxss/js)与Vue组件(.vue),后端基于SSM(Spring+SpringMVC+MyBatis)架构,数据库使用MySQL,配套完整建表SQL与系统部署说明。压缩包含1400个文件,主体为199个JS逻辑文件、148个Vue组件、136个Java业务类、107个JSON配置及96个WXML页面结构文件,辅以SVG图标、PNG素材与BAT一键部署脚本,总大小13.35MB,目录结构规范,模块划分清晰。已有178人学习下载,开发者可直接运行调试,快速掌握农户入驻、商品发布、用户下单、咨询互动、多角色权限管控等核心业务实现逻辑,并复用其分层架构设计与微信生态对接方案。
1. 为什么一个“农产品自主供销小程序”能成为毕业设计里的高分选题?
不是所有毕业设计都值得花三个月啃——但这个基于微信小程序 + Java 后端的农产品自主供销系统,是少数能同时踩中「业务真实感」「技术栈完整性」「部署可验证性」三个硬指标的选题。它不像“校园二手书平台”那样边界模糊,也不像“在线考试系统”那样同质化严重:农户上传滞销土豆、社区团长一键拼单、订单自动同步到 Java 后端生成入库单、微信支付回调触发库存扣减——整条链路全是可触摸的业务动作。我带过 7 届毕设,凡是把“自主供销”四个字真正落地(不是只写在需求文档里),答辩时老师问“订单超卖怎么防?”“小程序端图片上传失败怎么兜底?”“数据库字段加了索引但查询还是慢?”都能当场调出日志和 SQL 执行计划回答。它不追求炫技,但每个模块都有明确的技术锚点:小程序用 WXML/WXSS 做轻量交互,Java 后端用 Spring Boot + MyBatis-Plus 做事务控制,MySQL 设计要兼顾农户发布频次低、订单查询频次高的读写分离特征。适合计算机/软件工程专业学生,尤其推荐给想用毕设证明自己“能交付一个闭环系统”而非“会调 API”的人。
2. 小程序端:从农户发布到用户下单,5 个核心页面怎么搭才不翻车
2.1 首页轮播+分类导航:用 real-time database 还是云开发?实测选后者
很多同学一上来就想用云开发省事,但农产品场景下轮播图需支持后台动态配置(比如政府补贴活动 banner)、分类导航要关联后台商品类目表(蔬菜/水果/禽蛋/干货),硬套云开发的wx.cloud.database()会导致后续与 Java 后端数据同步困难。我的做法是:首页数据全部走 Java 接口,用wx.request调用/api/home/banner和/api/home/category。
// pages/index/index.js Page({ data: { banners: [], categories: [] }, onLoad() { this.loadHomeData(); }, loadHomeData() { wx.request({ url: 'https://your-java-server.com/api/home/banner', method: 'GET', success: (res) => { this.setData({ banners: res.data.list }); } }); wx.request({ url: 'https://your-java-server.com/api/home/category', method: 'GET', success: (res) => { this.setData({ categories: res.data.list }); } }); } });提示:接口返回结构必须严格约定。Java 后端用
@Data实体类封装List<BannerVO>,前端res.data.list直接赋值,避免res.data.data.list这种嵌套层级——这是新手最常写的玄学错误,导致 setData 为空。
2.2 农户发布页:图片压缩+多图上传的临界点控制
农户用手机拍的田间照片动辄 3–5MB,直接上传会卡死。小程序端必须做两件事:① 用wx.compressImage压缩到宽度 800px 以内;② 限制最多传 5 张图(超过弹窗提示“请精选5张代表图”)。关键不是“能传”,而是“传得稳”。
// pages/farmer/publish.js chooseImages() { wx.chooseMedia({ count: 5, mediaType: ['image'], sourceType: ['album', 'camera'], success: (res) => { const tempFiles = res.tempFiles; const compressedPromises = tempFiles.map(file => new Promise((resolve) => { wx.compressImage({ src: file.tempFilePath, quality: 60, // 压缩质量60%是临界点:再低失真,再高体积大 success: (compressRes) => resolve(compressRes.tempFilePath), fail: () => resolve(file.tempFilePath) // 压缩失败就传原图 }); }) ); Promise.all(compressedPromises).then(paths => { this.setData({ imagePaths: paths }); }); } }); }, uploadImages() { const uploadPromises = this.data.imagePaths.map((path, index) => wx.uploadFile({ url: 'https://your-java-server.com/api/image/upload', filePath: path, name: 'file', // 必须和后端 @RequestParam("file") 一致 formData: { type: 'product', index }, // 传序号,后端按序存 success: (res) => JSON.parse(res.data).url // 返回图片URL }) ); Promise.all(uploadPromises).then(urls => { // urls 是 ['https://.../1.jpg','https://.../2.jpg'] 数组 this.submitProduct(urls); }); }逻辑说明:wx.chooseMedia替代已废弃的wx.chooseImage,支持 iOS/Android 新版本;quality: 60是血泪经验——测试过 40/50/60/70 四档,60 在华为 P40 和 iPhone 12 上图片清晰度可接受,单图体积压到 300KB 内;formData里传index是为了后端能按顺序存图,避免“第一张图存成详情图,第二张存成封面图”的翻车。
2.3 商品详情页:长图文渲染与视频嵌入的兼容方案
农产品详情不能只靠文字,“现摘现发”需要视频佐证。但微信小程序<video>组件在 iOS 上有静音自动播放限制,安卓又存在首帧黑屏问题。绕过方案:用cover-view+canvas渲染首帧截图,点击后跳转全屏播放页。同时,长图文用rich-text组件,但必须处理<img>标签的宽高自适应——小程序默认img不占位,导致页面抖动。
<!-- pages/product/detail.wxml --> <view class="detail-content"> <rich-text nodes="{{content}}" bindtap="handleRichTextTap"></rich-text> </view>// pages/product/detail.js Page({ data: { content: '' }, onLoad(options) { const productId = options.id; wx.request({ url: `https://your-java-server.com/api/product/${productId}`, success: (res) => { // 后端返回的 content 是 HTML 字符串,含 <img> 和 <video> // 前端需替换 img src 为 https://.../xxx.jpg?imageView2/1/w/750/h/0 const html = res.data.content .replace(/<img src="(.*?)"/g, '<img src="$1?imageView2/1/w/750/h/0"') .replace(/<video.*?src="(.*?)".*?<\/video>/g, '<cover-view class="video-placeholder" bindtap="playVideo">@Service public class OrderService { @Resource private RedisTemplate<String, String> redisTemplate; @Resource private ProductMapper productMapper; @Resource private OrderMapper orderMapper; @Transactional(rollbackFor = Exception.class) public Result<OrderVO> createOrder(Long productId, Integer quantity, Long userId) { String lockKey = "order:lock:" + productId; Boolean locked = redisTemplate.opsForValue() .setIfAbsent(lockKey, "1", Duration.ofSeconds(10)); if (!locked) { return Result.fail("商品正在处理中,请稍候"); } try { // 1. 查询商品并校验库存(带 version) Product product = productMapper.selectById(productId); if (product.getStock() < quantity) { return Result.fail("库存不足"); } // 2. 乐观锁更新库存:WHERE id = ? AND version = ? int updated = productMapper.updateStockAndVersion( productId, quantity, product.getVersion()); if (updated == 0) { return Result.fail("库存已被抢完,请刷新重试"); } // 3. 创建订单 Order order = new Order(); order.setProductId(productId); order.setQuantity(quantity); order.setUserId(userId); orderMapper.insert(order); return Result.success(new OrderVO(order.getId())); } finally { redisTemplate.delete(lockKey); // 必须放 finally,防止异常不释放 } } }关键点说明:setIfAbsent设置锁过期时间 10 秒,比订单创建逻辑(通常 < 2 秒)长即可;updateStockAndVersion是自定义 XML SQL,用<if test="version != null">AND version = #{version}</if>拼接 WHERE 条件;finally中删锁是铁律,否则锁永远不释放。
3.2 图片上传:Nginx 反向代理 + 文件名防冲突
小程序上传图片到 Java 后端,若直接存服务器磁盘,扩容和备份极难。必须用对象存储(如腾讯云 COS),但 Java 后端不能暴露 COS 密钥给前端,所以走“后端签名直传”流程。Nginx 作反向代理,把/api/image/upload请求转发到 Java,Java 生成 COS 签名 URL 后返回给小程序,小程序直传 COS。
@RestController @RequestMapping("/api/image") public class ImageController { @Value("${cos.bucket}") private String bucket; @Value("${cos.region}") private String region; @PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file, @RequestParam("type") String type) { // 1. 生成唯一文件名:类型_时间戳_随机数.jpg String originalName = file.getOriginalFilename(); String ext = StringUtils.substringAfterLast(originalName, "."); String fileName = String.format("%s_%d_%s.%s", type, System.currentTimeMillis(), RandomUtil.randomString(6), ext); // 2. 生成 COS 签名 URL(有效期2小时) COSClient cosClient = new COSClient(cred, clientConfig); GeneratePresignedUrlRequest req = new GeneratePresignedUrlRequest( bucket, fileName, HttpMethodName.PUT); req.setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000)); URL signedUrl = cosClient.generatePresignedUrl(req); // 3. 返回签名 URL,小程序用 PUT 方法直传 return Result.success(signedUrl.toString()); } }注意:
RandomUtil.randomString(6)用 Hutool 工具类,避免UUID.randomUUID().toString()生成过长文件名;COS 签名 URL 必须用PUT方法上传,且请求头需带Content-Type(小程序端设置header: {'Content-Type': 'image/jpeg'})。
3.3 数据库设计:农户、商品、订单三张表的索引陷阱
很多同学建完表就跑,结果查订单列表慢得像拨号上网。核心三张表必须加这些索引:
| 表名 | 字段 | 索引类型 | 为什么必须 |
|---|---|---|---|
farmer | status,create_time | 联合索引 | 后台审核农户时查WHERE status=0 ORDER BY create_time DESC |
product | farmer_id,status,create_time | 联合索引 | 首页查“最新上架”商品,farmer_id是外键,status过滤未上架 |
order | user_id,status,create_time | 联合索引 | 用户查“我的订单”,user_id是高频查询条件 |
-- 执行前先看执行计划 EXPLAIN SELECT * FROM `order` WHERE user_id = 123 AND status IN (1,2) ORDER BY create_time DESC LIMIT 10;如果type是ALL(全表扫描),说明没走索引。避坑点:联合索引最左匹配原则。user_id必须是第一个字段,status第二个,create_time第三个——顺序错索引就废。
4. 避坑:这 4 个问题让 80% 的毕设项目在答辩前一周崩溃
4.1 现象:小程序真机调试正常,体验版打开白屏
原因:体验版域名未在微信公众平台配置合法 request 域名,或 Java 后端接口返回Access-Control-Allow-Origin: *但小程序要求精确匹配(如https://yourdomain.com)。
解决:登录 微信公众平台 → 开发管理 → 开发设置 → 服务器域名 → 添加https://your-java-server.com(注意是 HTTPS,且不能带路径);Java 后端用@CrossOrigin(origins = "https://your-java-server.com")精确指定,禁用*。
4.2 现象:农户发布商品后,后台看不到图片
原因:小程序上传图片时name字段写成'image',但 Java 后端@RequestParam("file")名称不一致;或 COS 上传时Content-Type未设置,导致 COS 存储为binary/octet-stream,前端访问时浏览器不识别。
解决:小程序端wx.uploadFile的name必须等于后端@RequestParam的 value;COS 上传时小程序 header 显式传'Content-Type': 'image/jpeg'(根据文件后缀动态设置)。
4.3 现象:订单支付成功,但数据库pay_status仍是 0
原因:微信支付回调地址/api/pay/callback未配置在微信商户平台,或 Java 后端未正确验签(WXPayUtil.isSignatureValid返回 false),导致回调被忽略。
解决:商户平台 → API 安全 → APIv3 密钥 → 下载证书;Java 后端用WXPayUtil.verifySignature验证回调签名,必须用证书解密resource.encrypted_message,不能直接解析原始 body。
4.4 现象:MySQL 查询SELECT * FROM product WHERE status=1走了全表扫描
原因:status字段未建索引,或建了单列索引但查询条件含OR(如WHERE status=1 OR is_hot=1)导致索引失效。
解决:对高频查询字段单独建索引ALTER TABLE product ADD INDEX idx_status (status);避免在 WHERE 中用OR,改用UNION ALL拆分查询。
5. 毕设答辩前必做的 3 件事:让老师一眼看出你“真做过”
5.1 用 JMeter 做一次 50 并发订单压测,截图放 PPT 第一页
别只说“系统稳定”,要证明。本地启动 JMeter,线程组设 50 个用户,循环 1 次,HTTP 请求指向/api/order/create,参数用 CSV Data Set Config 读取 50 个不同productId和userId。运行后看聚合报告:90% Line 的响应时间 < 800ms,错误率 0%,TPS > 15 —— 这比任何文字描述都有力。重点截图:聚合报告 + 查看结果树里任意一个成功请求的 Response Data(显示{code:0,msg:"success"})。
5.2 把数据库 ER 图导出为 PNG,标注主外键和索引字段
用 MySQL Workbench 连接你的数据库,右键 schema → “Reverse Engineer…” 生成 ER 图。导出为 PNG 后,在图上用箭头标出:product.farmer_id → farmer.id(外键),order.user_id字段旁写“idx_user_status_time 索引”。老师扫一眼就知道你懂关系建模,而不是瞎建表。
5.3 录制一段 60 秒操作视频:从农户发布→用户下单→后台查看订单
用 OBS 录屏,画面分三块:左侧小程序真机操作(发布草莓、下单)、中间 Postman 调用/api/order/list查订单、右侧 MySQL Workbench 执行SELECT * FROM order WHERE user_id=123。关键帧停在“订单状态为 1(已支付),库存减少 2 斤”画面上。视频不用配音,字幕打三行:“农户发布成功” → “用户支付完成” → “数据库实时更新”,干净利落。
最后说句实在话:这个毕设的价值不在代码多炫,而在于你亲手把“农户、消费者、后台管理员”三方角色串成一条数据流。我当年答辩被问“如果农户发错价格,怎么撤回?”,我直接打开小程序演示“发布后 2 小时内可编辑”,然后切到 Java 后端代码指出updateProduct方法里if (System.currentTimeMillis() - createTime < 2*60*60*1000)的判断逻辑——老师笑着点头,那刻我知道,这三个月没白熬。希望帮到你。
本文还有配套的精品资源,点击获取