
简介这是一份基于SSM框架的农产品自主供销微信小程序完整项目源码面向计算机相关专业毕业设计、小程序开发者及SSM框架学习者。资源整合农产品信息发布、在线购买、配送服务、支付结算、订单管理、评价反馈、促销活动与数据统计等核心模块可帮助读者理解小程序端与后端服务如何协同实现产销对接。压缩包共1400个文件包含136个Java后端类、148个Vue组件、199个JavaScript脚本、96个WXML页面及98个WXSS样式并配有PNG图片与SQL数据库脚本包体仅13.47MB便于快速下载与部署调试。目前已有184人学习浏览适合需要完整前后端分离案例、或希望基于SSM框架快速搭建电商类小程序的人群。内容预览可见清晰的工程目录与构建脚本如1-install.bat、2-run.bat、3-build.bat等内置主要页面与后台管理框架便于二次开发与功能扩展。1. 从农产品滞销到产销对接微信小程序如何改写供销链路农产品交易长期受制于信息不对称农民找不到稳定客源消费者买不到新鲜产地货。基于 SSMSpring SpringMVC MyBatis框架开发的微信小程序农产品自主供销平台核心逻辑是把“产地直发”搬到微信生态里——农民在小程序端发布产品信息、管理库存消费者在小程序里完成浏览、下单、支付、评价。技术选型上后端用 SSM 而非 Spring Boot是因为 SSM 在中小型交易系统中稳定可靠事务控制清晰适合做订单、库存这类强一致性业务。前端基于微信小程序原生框架无需下载 App扫码即用。这套源码适合正在做微信小程序毕业设计、接外包项目或想快速搭建农产品交易 MVP 的开发者。下文从架构分层、数据库设计、接口开发、支付对接、部署发布五个维度拆解附可直接复用的代码片段和参数说明。2. SSM 框架整合与小程序架构分层2.1 为什么在这个场景下选 SSM 而非 Spring Boot农产品供销平台并不是高并发互联网应用核心诉求是业务逻辑清晰、事务可靠、开发周期短。SSM 的三层架构——Spring 管 Bean 和事务、SpringMVC 管请求路由、MyBatis 管数据持久化——天然适合这种场景。Spring 的声明式事务可以用Transactional保证订单创建时库存扣减和订单记录写入的原子性避免超卖问题。!-- spring-mybatis.xml 关键配置 -- bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/farm_db?useUnicodetrueamp;characterEncodingutf8/ property nameusername valueroot/ property namepassword value123456/ !-- 连接池参数初始连接数、最大活跃连接数、最大等待时间 -- property nameinitialSize value5/ property namemaxActive value20/ property namemaxWait value60000/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ property nametypeAliasesPackage valuecom.farm.entity/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.farm.dao/ /bean这段配置里有两个容易被忽略的点useUnicodetrueamp;characterEncodingutf8是必须的农产品名称中大量出现生僻字和特殊符号编码不对直接导致乱码Druid 连接池的maxWait设置为 60 秒避免数据库连接被占满时请求无限阻塞。MyBatis 的typeAliasesPackage把实体类包路径注册进来后续在 Mapper XML 里写resultTypeProduct而不必写全限定名。2.2 小程序端目录结构与页面模块划分源码中的IndexMain.vue、IndexHeader.vue、BreadCrumbs.vue这些文件是后台管理端的前端组件运行在管理后台而非小程序端。小程序端的核心目录是pages/下的几个模块pages/index是商品列表首页、pages/product是商品详情、pages/order是订单列表、pages/cart是购物车、pages/user是个人中心。// app.json 页面注册与窗口配置 { pages: [ pages/index/index, pages/product/detail, pages/order/list, pages/cart/cart, pages/user/user ], window: { navigationBarTitleText: 农产品供销平台, navigationBarBackgroundColor: #4CAF50, navigationBarTextStyle: white, backgroundColor: #f5f5f5 }, tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/cart/cart, text: 购物车 }, { pagePath: pages/order/list, text: 订单 }, { pagePath: pages/user/user, text: 我的 } ] } }navigationBarBackgroundColor用了绿色和农产品天然属性匹配但更重要的是tabBar的配置——购物车和订单入口必须放在一级导航因为这两个模块是这个平台跳出率最低、使用频率最高的页面。如果后面要做 HBuilderX 的 uniapp 迁移这个配置结构可以直接映射到 uniapp 的pages.json改动成本很低。2.3 小程序登录态与后端 Session 交互机制微信小程序的登录和 Web 端完全不同核心差异在于小程序不能直接操作 Cookie必须通过wx.login获取临时凭证code再由后端调用微信接口换取openid。// 小程序端登录逻辑 wx.login({ success: (res) { if (res.code) { wx.request({ url: https://api.farm.com/user/login, data: { code: res.code }, success: (res) { const token res.data.data.token; // 关键token 必须存入本地缓存后续所有请求都要带上 wx.setStorageSync(token, token); } }); } } });后端的处理流程是接收code调用https://api.weixin.qq.com/sns/jscode2session换取openid和session_key用openid查数据库——如果用户存在则直接登录不存在则自动注册最后生成自定义 token 返回给前端。// UserController.java 登录接口核心逻辑 RestController RequestMapping(/user) public class UserController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginRequest req) { // 1. 通过 code 调用微信接口获取 openid String openid wechatService.getOpenId(req.getCode()); if (openid null) { return Result.error(微信登录失败); } // 2. 根据 openid 查找用户不存在则自动注册 User user userService.findByOpenId(openid); if (user null) { user new User(); user.setOpenId(openid); user.setNickname(微信用户 openid.substring(openid.length() - 6)); user.setRole(1); // role: 1 消费者, 2 农民 userService.register(user); } // 3. 生成 token 并返回这里用 UUID生产环境建议用 JWT String token UUID.randomUUID().toString().replaceAll(-, ); redisService.set(token: token, user.getId().toString(), 7 * 24 * 3600); return Result.success(token); } }这段代码里的关键参数是role字段——消费者和农民共用一套登录逻辑通过role区分身份农民登录后多一个“发布商品”的入口。token 的有效期设置为 7 天过期后用户需要重新登录这种方式的优点是实现简单缺点是分布式部署时需要共享 Redis不过单机部署场景完全够用。3. 农产品商品管理与订单状态机的核心实现3.1 商品信息发布模块多图上传与类目绑定农民端发布农产品是整个平台的供应链起点。这个模块的技术难点不在后端而在前端的上传体验和后端的字段设计。小程序端使用wx.chooseImage选择图片、wx.uploadFile上传到后端接口后端用 MultipartFile 接收并存储到服务器指定目录。// 小程序端发布商品页核心代码 submitProduct() { const product { name: this.data.name, // 产品名称 origin: this.data.origin, // 产地 categoryId: this.data.categoryId, // 类目 ID price: this.data.price, // 单价元/斤 stock: this.data.stock, // 库存斤 unit: this.data.unit, // 单位 description: this.data.description // 描述 }; // 先上传图片拿到 URL 后再提交表单数据 if (this.data.images.length 0) { this.uploadImages(0, product); } else { this.submitProductData(product); } }, uploadImages(index, product) { wx.uploadFile({ url: https://api.farm.com/product/upload, filePath: this.data.images[index], name: file, success: (res) { const data JSON.parse(res.data); product.images.push(data.data.url); if (index this.data.images.length - 1) { this.uploadImages(index 1, product); } else { this.submitProductData(product); } } }); }注意这里的递归上传逻辑——wx.uploadFile不支持并发上传多个文件所以用递归方式逐个上传。生产环境建议加一个上传进度条组件否则多图上传时用户会以为卡死了。后端对应的 Controller 用RequestParam(file) MultipartFile file接收文件大小上限在spring-mvc.xml里配置!-- spring-mvc.xml 上传文件大小限制 -- bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver !-- 单个文件最大 5MB -- property namemaxUploadSize value5242880/ !-- 请求编码必须和页面保持一致 -- property namedefaultEncoding valueUTF-8/ /bean农产品图片通常包含田间实拍和产品特写5MB 的限制是合理的——原图太大既浪费存储空间也拖慢加载速度。实际部署时可以再加一层 Nginx 做图片访问的静态资源映射把product/upload接口返回的相对路径拼上 Nginx 的域名前缀即可。3.2 购物车与下单流程事务边界与库存强校验购物车是交易系统的典型场景。用户把商品加入购物车、调整数量、选中结算最后生成订单。后端的核心接口是“创建订单”——这一步涉及多张表的写入订单主表、订单明细表、商品库存扣减必须放在同一个事务里。// OrderService.java 创建订单 Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, ListCartItem items, Address address) { // 计算订单总金额 BigDecimal totalAmount BigDecimal.ZERO; ListOrderItem orderItems new ArrayList(); for (CartItem item : items) { Product product productMapper.selectById(item.getProductId()); if (product null || product.getStatus() ! 1) { throw new BusinessException(商品已下架 product.getName()); } // 库存校验必须用 SELECT ... FOR UPDATE 锁定行 Integer stock productMapper.selectStockForUpdate(product.getId()); if (stock item.getQuantity()) { throw new BusinessException(库存不足 product.getName()); } // 计算小计 BigDecimal subtotal product.getPrice().multiply(new BigDecimal(item.getQuantity())); totalAmount totalAmount.add(subtotal); OrderItem orderItem new OrderItem(); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItem.setSubtotal(subtotal); orderItems.add(orderItem); } // 插入订单主表 Order order new Order(); order.setUserId(userId); order.setOrderNo(generateOrderNo()); order.setTotalAmount(totalAmount); order.setStatus(0); // 0 待支付, 1 待发货, 2 待收货, 3 已完成, 4 已取消 order.setAddressDetail(address.getDetail()); order.setReceiverName(address.getName()); order.setReceiverPhone(address.getPhone()); orderMapper.insert(order); // 插入订单明细 for (OrderItem item : orderItems) { item.setOrderId(order.getId()); orderItemMapper.insert(item); // 扣减库存 productMapper.decreaseStock(item.getProductId(), item.getQuantity()); } // 清空购物车中已结算的商品 cartMapper.deleteByUserIdAndProductIds(userId, items.stream() .map(CartItem::getProductId).collect(Collectors.toList())); return order.getId(); }这段代码里最关键的是selectStockForUpdate方法——MyBatis 对应的 SQL 是SELECT stock FROM product WHERE id #{id} FOR UPDATE它在数据库层面锁定该商品的行防止两个用户同时下单导致超卖。Transactional确保如果后面任何一步抛异常前面的数据库操作全部回滚不会出现扣了库存但订单没创建的脏数据。参数说明rollbackFor Exception.class表示所有异常包括自定义的BusinessException都会触发回滚如果是 Spring Boot 项目默认只回滚RuntimeException。status字段是订单状态机的核心0 到 4 的流转严格单向0 只能进到 1 或 41 只能进到 22 只能进到 3。订单号的生成规则建议用“时间戳 6 位随机数”避免用数据库自增 ID 直接暴露订单量。3.3 订单列表与状态流转下拉刷新与分页加载的配合订单列表是小程序端最常用的页面之一。消费者需要看到待支付、待发货、已完成等不同状态的订单。前端用wx.pageScrollTo配合onReachBottom实现分页加载每页 10 条后端用 MyBatis 的PageHelper插件实现分页。// OrderMapper.xml 分页查询订单列表 select idselectOrderPage resultTypeOrder SELECT * FROM order WHERE user_id #{userId} if teststatus ! null and status ! -1 AND status #{status} /if ORDER BY create_time DESC /selectPageHelper的使用方式是在 Service 层调用前设置分页参数// OrderService.java public PageInfoOrder getUserOrders(Long userId, Integer status, int pageNum, int pageSize) { PageHelper.startPage(pageNum, pageSize); ListOrder orders orderMapper.selectOrderPage(userId, status); return new PageInfo(orders); }这里有个细节PageHelper.startPage只对接下来执行的第一个 MyBatis 查询生效所以如果有多个查询务必先把分页查询写在前面的位置。小程序端对应的请求参数是pageNum和pageSizeonReachBottom触发时页数加 1并把返回的数据追加到原有列表后面同时用一个isLoading标志防止重复请求。4. 支付对接与配送模块的关键问题规避4.1 微信支付 JSAPI 下单完整链路农产品商城必须支持微信支付这是交易闭环的最后一环。微信小程序的支付走 JSAPI 支付通道和公众号支付的区别在于openid的获取方式——小程序通过wx.login获取code后端再向微信支付统一下单接口请求。整个链路如下第一步后端生成预支付交易单// WechatPayService.java 统一下单 public MapString, String createPrepayOrder(Long orderId, BigDecimal amount, String openid) { // 1. 构造参数 SortedMapString, String params new TreeMap(); params.put(appid, wechatConfig.getAppId()); params.put(mch_id, wechatConfig.getMchId()); params.put(nonce_str, generateNonceStr()); params.put(body, 农产品订单- orderId); params.put(out_trade_no, orderMapper.selectById(orderId).getOrderNo()); params.put(total_fee, amount.multiply(new BigDecimal(100)).intValue() ); params.put(spbill_create_ip, getClientIp()); params.put(notify_url, wechatConfig.getNotifyUrl()); params.put(trade_type, JSAPI); params.put(openid, openid); // 2. 生成签名 String sign generateSign(params, wechatConfig.getApiKey()); params.put(sign, sign); // 3. 转成 XML 发送请求 String xmlData mapToXml(params); String response httpClient.post(wechatConfig.getUnifiedOrderUrl(), xmlData); MapString, String result xmlToMap(response); // 4. 返回小程序端需要的参数 MapString, String payParams new HashMap(); payParams.put(timeStamp, System.currentTimeMillis() / 1000 ); payParams.put(nonceStr, result.get(nonce_str)); payParams.put(package, prepay_id result.get(prepay_id)); payParams.put(signType, MD5); payParams.put(paySign, generateSign(payParams, wechatConfig.getApiKey())); return payParams; }第二步小程序端发起支付// 小程序端支付代码 wx.requestPayment({ timeStamp: payParams.timeStamp, nonceStr: payParams.nonceStr, package: payParams.package, signType: MD5, paySign: payParams.paySign, success: (res) { // 支付成功后的跳转逻辑 wx.navigateTo({ url: /pages/order/detail?id orderId }); }, fail: (err) { wx.showToast({ title: 支付取消, icon: none }); } });参数陷阱自查表参数易错点正确做法total_fee单位为分不能传元元转分用multiply(new BigDecimal(100)).intValue()out_trade_no不能超过 32 位用订单号字段不用自增 IDspbill_create_ip不能传 null获取客户端真实 IP小程序端可传127.0.0.1的替代值sign必须使用 ASCII 码排序用 TreeMap 保证按键名自然排序notify_url必须是公网可访问的 HTTPS不能用 localhost 或内网 IP4.2 支付结果回调与订单状态更新支付回调是微信支付最关键的坑点。微信服务器会以 POST 形式把支付结果推送到notify_url后端需要在回调接口里做三件事验签、校验订单金额、更新订单状态。特别注意回调接口返回的响应必须是 XML 格式的success字样否则微信会认为回调失败并持续重试。// WechatPayCallbackController.java 支付回调处理 RequestMapping(value /wechat/pay/notify, method RequestMethod.POST) ResponseBody public String payNotify(HttpServletRequest request) throws Exception { // 1. 读取请求体 XML String xmlData readRequestBody(request); MapString, String params xmlToMap(xmlData); // 2. 验签 if (!checkSign(params, wechatConfig.getApiKey())) { return returnXml(FAIL, 签名错误); } // 3. 校验业务结果 if (SUCCESS.equals(params.get(result_code))) { String orderNo params.get(out_trade_no); String transactionId params.get(transaction_id); String totalFee params.get(total_fee); // 4. 更新订单状态必须先查订单校验金额 Order order orderMapper.selectByOrderNo(orderNo); if (order ! null order.getStatus() 0) { // 防止重复回调status 已经是 1 则不再更新 if (order.getTotalAmount().multiply(new BigDecimal(100)) .intValue() ! Integer.parseInt(totalFee)) { return returnXml(FAIL, 金额不一致); } orderMapper.updateStatus(order.getId(), 1, transactionId); return returnXml(SUCCESS, OK); } } return returnXml(FAIL, 未知错误); }在updateStatus的 Mapper SQL 里必须加上WHERE id #{id} AND status 0条件用数据库行锁的机制天然防重复更新。如果在代码层面做判断高并发下两个回调请求同时进入还是会存在竞态问题。4.3 配送范围与配送时间的数据建模配送模块是这个平台区别于普通电商的特有功能。农产品的配送范围通常限制在同城或周边区域需要在数据库里建配送区域表并和运费、预计送达时间关联。CREATE TABLE delivery_config ( id INT PRIMARY KEY AUTO_INCREMENT, region_name VARCHAR(50) COMMENT 配送区域名称如北京市海淀区, delivery_fee DECIMAL(10,2) DEFAULT 0 COMMENT 配送费用元, free_above DECIMAL(10,2) DEFAULT 0 COMMENT 满额起送元, estimated_days INT DEFAULT 1 COMMENT 预计送达天数, status TINYINT DEFAULT 1 COMMENT 1 启用, 0 禁用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里的设计思路是消费者在收货地址里选择区域后端根据region_name匹配配送配置计算配送费和预计送达时间。estimated_days用整数而不是时间范围避免复杂的日期计算前端展示时直接拼接成“预计 2 天内送达”。如果是同城配送且当天送达可以加一个same_day_deadline字段用于判断当前时间是否在当日截单时间之前。5. 部署发布与后期维护的实战细节5.1 源码包内的批处理脚本到底做了什么项目源码根目录下有三个批处理文件1-install.bat、2-run.bat、3-build.bat。这三个脚本对应三个阶段的自动化流程虽然代码极简但用好了能省很多时间。:: 1-install.bat —— 执行数据库脚本并初始化环境 echo off echo 开始初始化数据库... mysql -uroot -p123456 sql/init.sql echo 数据库初始化完成 :: 安装后端依赖如果有 Maven 配置 call mvn clean install -DskipTests pause:: 2-run.bat —— 启动后端服务 echo off set MAVEN_OPTS-Xms256m -Xmx512m call mvn spring-boot:run pause注意这里虽然项目是 SSM 框架但也可以用spring-boot:run启动——说明项目可能已经内嵌了 Spring Boot 的启动类这种情况在课程设计中很常见SSM 的代码结构 Spring Boot 的启动方式。实际部署时建议改用java -jar方式将项目打成 war 包放到 Tomcat webapps 目录下或在 IDE 中直接 Debug 模式启动。:: 3-build.bat —— 前端打包如果存在前端工程 echo off cd frontend call npm install call npm run build cd .. echo 前端构建完成特别说明源码包里的.bak文件如update-password.vue.bak、IndexMain.vue.bak是开发过程中备份的旧版本文件说明在管理后台开发中经历过多次页面调整。这些文件可以安全删除不影响项目运行。如果提示端口冲突优先检查 8080 端口是否被占用。5.2 Charles 抓包与加载页面定制调试在小程序开发调试过程中抓包是排查接口问题的核心手段。电脑端微信小程序的抓包思路是将微信开发者工具的网络代理指向 Charles在 Charles 中配置 SSL 代理即可看到小程序端发起的所有 HTTPS 请求详情包括请求参数、响应数据、HTTP 状态码。具体步骤如下Charles 菜单栏 Proxy → SSL Proxying Settings → 勾选 Enable SSL Proxying并添加 Host 为api.farm.com、Port 为 443微信开发者工具右上角详情 → 本地设置 → 勾选“不校验合法域名”打开 Charles在微信开发者工具中操作小程序即可在 Charles 的 Structure 或 Sequence 视图中看到请求记录排查要点如果接口请求显示ERR_CERT_COMMON_NAME_INVALID说明小程序的合法域名校验未关闭如果看到blocked:csp字样说明微信开发者工具的安全策略拦截了请求。抓包完成后要记得将代理关闭否则会影响其他应用联网。加载页面的定制也是常见需求。微信小程序冷启动时展示的 loading 页面其实是app.json中window配置的navigationBarTitleText和自定义的启动背景图。修改项目根目录下的pages/index/index页面在onLoad生命周期里调用接口获取最新的数据// pages/index/index.js 定制首页加载逻辑 Page({ data: { banners: [], products: [], loading: true }, onLoad() { // 同时请求 banner 和商品列表使用 Promise.all 缩短加载时间 Promise.all([ this.fetchBanners(), this.fetchProducts() ]).then(() { this.setData({ loading: false }); }); } })5.3 农产品统计报表销售额与用户行为分析的数据口径数据统计与分析是管理后台的核心附加价值。根据摘要描述系统需要统计销售额、订单数量、用户行为等指标。这类统计功能的实现要注意“口径问题”——比如销售额是按订单创建时间算还是按支付成功时间算建议统一按支付成功时间统计因为存在用户下单后不付款的无效订单。// StatisticsMapper.xml 按天统计销售额与订单量 select idselectDailySales resultTypeDailySalesVO SELECT DATE_FORMAT(pay_time, %Y-%m-%d) AS stat_date, COUNT(*) AS order_count, SUM(total_amount) AS sales_amount FROM order WHERE status IN (1, 2, 3) -- 排除待支付和已取消的订单 AND pay_time #{startDate} AND pay_time lt; #{endDate} GROUP BY DATE_FORMAT(pay_time, %Y-%m-%d) ORDER BY stat_date DESC /select在 MyBatis XML 中小于号必须写成lt;否则会因 XML 解析报错。另外注意SUM(total_amount)在订单表为空时会返回 NULL 而不是 0前端拿到 NULL 会造成渲染错误所以建议用IFNULL(SUM(total_amount), 0)兜底。用户行为分析可以用user_id统计活跃用户数、用订单表统计复购率这些指标配合ECharts小程序版ec-canvas组件渲染成柱状图和折线图。还有一个实际运维中的高频问题小程序修改了app.json后在开发者工具中上传新版本管理员需要登录微信公众平台在“版本管理”中把新版本选为“体验版”或“提交审核”——这一步很多新手会卡住以为上传后用户立即能看到实际上微信小程序必须经过提审和发布流程线上用户不会自动收到更新需要配置“自动更新”或用户刷新后才会拉取新版。本文还有配套的精品资源点击获取