基于SSM与微信小程序的农产品自主供销系统开发指南
2026/9/15 4:18:11 网站建设 项目流程

简介:基于SSM(Spring+SpringMVC+MyBatis)框架开发的微信小程序农产品自主供销系统,是一套覆盖商品发布、在线下单、支付配送的完整前后端工程,适合微信小程序开发者、Java后端程序员以及正在准备毕业设计的高校学生。压缩包共含1400个文件,以Java代码、Vue组件、JavaScript脚本、WXML/WXSS页面文件为主,其中Java和Vue承担服务端逻辑及后台管理界面,WXML/WXSS结合JS构成小程序前端页面,另附有SQL数据库脚本、XML配置和Maven相关文件,整体仅13.47MB,目录结构清晰,便于本地导入和二次开发。当前已有184人学习下载,具备较强的工程参考价值。读者可从中获取完整的小程序设计思路、服务端接口实现、后台管理页面布局、数据库表设计方式,以及农产品信息发布、订单跟踪、促销活动等核心模块的落地方法;从内容预览看,资源还包含了安装运行脚本和备份文件,可辅助快速搭建环境,适合在毕业设计或课程项目中直接复用。

1. 农产品自主供销小程序,为什么选 SSM + 微信小程序

拿到类似“ssm框架基于微信小程序的农产品自主供销小程序.rar”这种项目包,第一件事不是解压跑起来,而是先拆清楚标题里的三块:SSM 负责什么,微信小程序负责什么,“自主供销”到底要做成什么形态。很多人把农产品项目做成了静态展示页,商品摆上去却不能下单,不能扣库存,也不能处理订单,那只能叫农产品黄页。真正的自主供销,至少要覆盖商品上架、选购下单、库存扣减、支付或货到付款、订单履约这几条链路,缺一条都不完整。

SSM 是 Spring + SpringMVC + MyBatis 的经典组合,服务端承担接口、事务、SQL 操作;微信小程序承担买家端交互,通过 HTTPS 与后端交换 JSON,不直接碰数据库。这套分层在中小型项目里足够清晰,也正好是很多课程设计和毕业设计的选型。它适合两类人:一是要把农产品商城从 0 到 1 跑通的学生团队,二是在老 SSM 项目上做二次开发的工程师。下面按服务端、小程序端、供销难点和导出工具的路径,把一套可落地的做法讲清楚。

2. SSM 框架服务端:搭好农产品接口的第一层

2.1 为什么还在用 SSM,而不是直接换成 Spring Boot

Spring Boot 本质上仍然是 Spring,只是把自动配置、内嵌容器和起步依赖都封装好了。SSM 的区别在于,你要自己维护web.xmlspring-mvc.xmlmybatis-config.xml,这虽然繁琐,但能让你看清 DispatcherServlet 是怎么注册的、SqlSessionFactory 是怎么被 Spring 管理的。很多现存的管理后台、课程设计模板仍然跑在 SSM 上,因为课程大纲按 Spring、SpringMVC、MyBatis 三块来教,所以“SSM 框架”四个字才会出现在项目标题里。

拿到项目包后,先确认构建方式。如果是 Maven 工程,看pom.xml里依赖版本是否冲突;如果直接把 jar 包放在WEB-INF/lib下,就很容易出现同一个类被不同版本重复加载。我一般会先抽出一个最简单的接口,比如查商品列表,把链路跑通后再往里面加订单和库存逻辑。

2.2 核心依赖版本要对齐,否则启动就报类找不到

SSM 对版本很敏感。Spring 5.3.x 搭配 MyBatis 3.5.x 是常见组合,Spring 6 或 Spring Boot 3 已经把javax.*换成了jakarta.*,传统 SSM 项目不要直接升上去。下面这组依赖可以放在pom.xml中作为起点。

<dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.39</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.16</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.1.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

这里的版本号不必完全照抄,关键原则是 Spring 5.3.x 搭配 MyBatis 3.5.x 和 mybatis-spring 2.1.x,驱动用 MySQL 8 就选com.mysql.cj.jdbc.Driver,不要用已经移除的com.mysql.jdbc.Driver。如果项目中同时存在 Spring 4 和 Spring 5 的包,启动时会大量出现NoSuchMethodError,那不是代码写错,而是依赖没有收敛。

2.3 农产品商品表设计:把供销基础字段先定住

自主供销的前提是商品能上架、能下架、能控制库存。常见做法是设计两张基础表:product存商品,category存分类,order 相关放到后面订单章节。下面这组字段可以满足大部分农产品商城场景。

字段名类型说明
idBIGINT主键,自增
nameVARCHAR(100)农产品名称
category_idBIGINT分类 id
priceDECIMAL(10,2)销售价,以后端计算为准
stockINT剩余库存
image_urlVARCHAR(255)图片地址(小程序端直接用)
weightDECIMAL(10,3)单件重量,单位 kg,用于运费计算
statusTINYINT1 上架,0 下架
create_timeDATETIME创建时间

对应的建表 SQL 里要注意字符集用utf8mb4,否则农产品名称里的特殊字符可能存不进去。下面这段可以直接用于初始化。

CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT '农产品名称', category_id BIGINT NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image_url VARCHAR(255) DEFAULT '', weight DECIMAL(10,3) DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

status字段很关键,小程序端商品列表默认只查status = 1的商品,下架操作不要物理删除,这样订单关联的商品快照还能保证完整性。weight字段在计算运费时用,别放在备注里。

2.4 写一个商品列表接口:从 Controller 到 Mapper

商品列表是供销小程序最基础的接口,也是整条链路能否跑通的验证点。Controller 层暴露 REST 接口,Service 层处理分页逻辑,Mapper 层直接写 SQL。这里给一个最简实现。

@RestController @RequestMapping("/api/product") public class ProductController { @Autowired private ProductService productService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size) { return Result.ok(productService.pageOnSale(page, size)); } }

Result是统一返回体,通常包含codemessagedata三个字段,小程序端所有接口都解析同一个结构。pagesize是分页参数,默认值写在注解里,防止前端漏传导致 SQL 异常。Mapper 层用 XML 写 SQL,方便以后加复杂查询。

<select id="listOnSale" resultType="com.example.pojo.Product"> SELECT id, name, price, stock, image_url AS imageUrl FROM product WHERE status = 1 ORDER BY id DESC LIMIT #{offset}, #{size} </select>

image_url AS imageUrl是因为 Java 属性叫imageUrl,数据库字段是image_url。如果你在mybatis-config.xml里开启了mapUnderscoreToCamelCase=trueAS可以省略,系统会自动做驼峰映射。LIMIT的两个参数,第一个是偏移量,第二个是每页条数,Service 层计算时通常是(page - 1) * size

2.5 联调前先在微信开发者工具里取消域名校验

小程序端请求后端接口,如果后端只用了局域网 IP,比如http://192.168.1.10:8080,在真机上必须打开“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”这个选项,否则一个请求都发不出去。这个选项不是上线配置,只是本地联调用的。我一般会让后端在电脑上的 Tomcat 里跑起来,小程序用同一台机器的浏览器能先访问到接口,再处理具体字段。

3. 微信小程序端:供销商城的四个页面骨架

3.1 页面划分和 tabBar 配置

农产品供销小程序最常见的结构是四个页签:首页商品列表、购物车、订单、个人中心。app.json里的tabBar会直接决定启动后能看到哪些页签,以下是一个参考值。

{ "pages": [ "pages/index/index", "pages/cart/cart", "pages/order/list/list", "pages/user/user" ], "tabBar": { "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/order/list/list", "text": "订单" }, { "pagePath": "pages/user/user", "text": "我的" } ] } }

pagePath必须与pages数组里的路径完全一致,否则 tabBar 显示不出来。四个页面里,订单列表页不是简单展示,而是根据订单状态切换“待付款”“待发货”“已完成”等视图,这会在小程序端做状态过滤,不需要每次都请求后端。

3.2 request 封装和微信小程序登录

小程序端不能像浏览器一样直接跨域,但wx.request本身不受浏览器的同源策略限制,真正限制是必须使用 HTTPS 并配置合法域名。开发环境下可以暂时忽略。我习惯先封装一个request.js,把 baseURL 和 token 统一处理。

const BASE_URL = 'http://localhost:8080'; function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: reject }); }); } module.exports = { request };

这段封装把后端返回的code统一判断为 0 表示成功,这样每个页面只需要关心业务数据。Authorization头用来携带登录后的 token,如果用户还没登录,后端会返回 401,再触发wx.loginwx.login拿到的 code 只能使用一次,而且要尽快发给后端。

wx.login({ success: (res) => { request('/api/user/login', 'POST', { code: res.code }) .then((data) => { wx.setStorageSync('token', data.token); }); } });

后端拿到 code 后,通过微信接口换 openid,再生成自定义 token 返回。小程序端不要直接拿 openid 做登录态,因为它是敏感信息,也不应该暴露给前端页面。

3.3 商品列表页:首页加载的最小实现

商品列表页的常见做法是在onLoad里请求接口,然后setData渲染列表。要注意的是setData有传输大小限制,列表数据量太大时需要分页。

Page({ data: { products: [], page: 1, size: 10, loading: false, hasMore: true }, onLoad() { this.loadProducts(); }, loadProducts() { if (this.data.loading || !this.data.hasMore) return; this.setData({ loading: true }); request(`/api/product/list?page=${this.data.page}&size=${this.data.size}`) .then((data) => { this.setData({ products: this.data.products.concat(data.list), page: this.data.page + 1, hasMore: data.list.length === this.data.size }); }) .finally(() => { this.setData({ loading: false }); }); } });

hasMore用来判断是否还有下一页,避免滚动到底部后不断重复请求。.finally在开发者工具里需要库版本支持,不然可以用complete回调代替。商品列表页的view层只需要渲染namepriceimageUrl,加购物车按钮单独绑定>function addToCart(product) { const cart = wx.getStorageSync('cart') || {}; const key = product.id; if (cart[key]) { cart[key].count += 1; } else { cart[key] = { ...product, count: 1 }; } wx.setStorageSync('cart', cart); wx.showToast({ title: '已加入购物车', icon: 'success' }); }

key用商品 id 保证唯一。购物车数据里保存priceimageUrl只是为了展示,提交订单时不能直接用购物车里的价格,必须以服务端重新查询出来的价格为准。如果要做多端同步购物车,再考虑建cart_item表,否则本地存储更省事。

3.5 修改刚进入的加载页面:入口页和启动占位页

很多小程序启动时先进入pages/index/index,但如果你想做宣传图或者广告页,可以用entryPagePath指定入口页。以下配置会让用户先到pages/launch/launch

{ "pages": [ "pages/launch/launch", "pages/index/index", "pages/cart/cart" ], "entryPagePath": "pages/launch/launch" }

launch页的onLoad里,用wx.reLaunch跳转到首页。reLaunch会关闭所有页面,避免用户按返回键回到启动页。注意entryPagePath指定的页面也要在pages列表里注册,否则编译直接报错。这是“修改刚进入的加载页面”最直接的做法,不需要改原生启动图。

4. 供销场景的难点:库存、配送与支付回调

4.1 库存扣减用条件更新,防止超卖

农产品有很强的季节性,库存数字如果和实际对不上,后面订单履约就会出问题。最常见的错误是先select stock,再在 Java 里判断是否够用,然后update,这在高并发下会超卖。正确做法是在一条 SQL 里完成扣减和条件校验。

UPDATE product SET stock = stock - #{quantity} WHERE id = #{productId} AND stock >= #{quantity}

JDBC 的update方法会返回受影响行数,如果返回 0,说明库存不足,整个事务应该回滚。在 Service 方法上加上@Transactional,保证订单创建和库存扣减要么都成功,要么都失败。

@Transactional public void createOrder(Long productId, Integer quantity) { int rows = productMapper.deductStock(productId, quantity); if (rows == 0) { throw new BusinessException("库存不足"); } orderMapper.insert(order); }

deductStock返回的rows是数据库更新的行数,不是查询结果。库存字段要设置成INT NOT NULL,不要允许为空,否则stock >= #{quantity}的判断会失效。注意@Transactional只对运行时异常生效,不要捕获异常后吞掉。

4.2 订单价格以后端计算为准,前端价格只能展示

小程序端购物车里的price是本地缓存,用户修改商品规格、后台改价格或者做促销,都会让前端价格失真。生成订单时,后端必须根据商品 id 重新从数据库读取价格,再和数量相乘。下面是一个订单项计算的简化逻辑。

public BigDecimal computeOrderAmount(List<Long> productIds, Map<Long, Integer> quantities) { BigDecimal total = BigDecimal.ZERO; for (Long productId : productIds) { Product product = productMapper.selectById(productId); if (product == null || product.getStatus() != 1) { throw new BusinessException("商品已下架"); } BigDecimal itemAmount = product.getPrice() .multiply(BigDecimal.valueOf(quantities.get(productId))); total = total.add(itemAmount); } return total; }

BigDecimal不能用double计算,否则精度会出问题,尤其农产品经常出现“9.9 元 3 斤”这类小数。商品下架后,前端可能还能看到缓存图片,但下单请求必须被后端拦截。

4.3 配送参数:自提点选择和运费模板

农产品自主供销不一定走快递,很多场景是“线上下单、线下自提”或者“同城配送”。订单表里需要增加一个delivery_type字段,1 表示自提,2 表示配送。自提要存自提点 id,配送要存收货地址。运费按重量计算是农产品商城比较合理的方案,因为一箱苹果和三斤大米的运费差别很大。

参数名类型说明
delivery_typeTINYINT1 自提,2 配送
address_idBIGINT配送地址 id,自提时为空
pickup_point_idBIGINT自提点 id,配送时为空
freightDECIMAL(10,2)运费金额

运费计算可以放在后端,起步价和续重价做成配置。简单做法是先算总重量,再判断是否超过首重,超过部分按续重单价累加。自提模式下运费直接置 0,不需要走快递逻辑。

4.4 支付回调:接入微信支付前先确认三件事

如果农产品供销要支持线上支付,微信支付 v3 是绕不开的。小程序的wx.requestPayment需要后端先调用微信支付统一下单接口,拿到prepay_id再生成支付参数。支付回调是异步的,后端必须验签,不能只靠前端返回的 success 就改订单状态。

第一,回调地址必须是 HTTPS,且不能带查询参数;第二,回调通知里的resource是 AES-256-GCM 加密数据,需要用 API v3 密钥解密;第三,同一个订单可能收到多次回调,需要先查订单状态,如果已经是已支付就直接返回成功,防止重复发货。

if (order.getStatus() == OrderStatus.PAID) { return "{\"code\":\"SUCCESS\"}"; } orderMapper.updateStatus(order.getId(), OrderStatus.PAID);

这段代码放在回调处理逻辑的最前面,目的就是幂等。很多项目在这里没有判断,结果同一订单被回调两次,导致库存又扣了一遍。后续的库存扣减如果放在下单时做,退款要回补库存;如果放在支付后做,未支付订单不占库存,但需要定时清理过期订单。

4.5 联调排错:抓包和高频问题对照

小程序和后端联调时,Charles 是常用的抓包工具。抓电脑端微信小程序时,需要开启 SSL Proxying,并安装 Charles 根证书;抓手机端时,让手机 WiFi 指向电脑,再安装证书。注意微信小程序的wx.request并发数限制是 10 个,超过的请求会排队,商品列表图片太多时会表现为页面卡顿。

现象常见原因处理方式
requestfail url not in domain list没有把接口地址加入小程序后台合法域名开发环境勾选不校验域名,上线前加白名单
真机上图片不显示image_url是 HTTP 地址改用 HTTPS,或者开启域名校验时加入图片域名
提交订单后库存不变事务没生效或异常被吞确认@Transactional在 Spring 容器管理的方法上
iOS 上页面滚动卡顿大量setData且数据量大列表页用wx:for分批渲染,减少大对象传递

这里还有一个 iOS 微信小程序渲染机制的特殊坑:如果日期选择器组件放在scroll-view里,滚动事件容易被父容器抢走,导致选择器打开后立即关闭。常见做法是让日期选择器浮层脱离滚动容器,用position: fixed定位到屏幕中间,再绑定catchtouchmove阻止透传。

5. 订单导出 Excel:给农户和管理员的日常工具

5.1 后端生成 xlsx 文件并返回下载地址

农产品供销项目做完交易闭环后,最常用的功能就是订单导出。农户需要每天看卖了多少、管理员需要按日期对账,这时候生成 Excel 比页面里一页一页翻效率高很多。后端可以用 Apache POI 生成.xlsx文件,写入订单号、商品名、数量、金额、收货信息和订单状态。

@GetMapping("/api/order/export") public void export(@RequestParam String date, HttpServletResponse response) throws IOException { List<OrderVO> orders = orderService.listByDate(date); try (Workbook workbook = new XSSFWorkbook()) { Sheet sheet = workbook.createSheet("订单导出"); String[] headers = {"订单号", "商品", "数量", "金额", "状态", "下单时间"}; Row headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 写入订单数据 for (int i = 0; i < orders.size(); i++) { Row row = sheet.createRow(i + 1); row.createCell(0).setCellValue(orders.get(i).getOrderNo()); // 省略具体赋值 } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=orders_" + date + ".xlsx"); workbook.write(response.getOutputStream()); } }

XSSFWorkbook对应.xlsxHSSFWorkbook对应.xls,后者最大只支持 65536 行,订单量大的时候会直接报错,所以优先用 XSSF。响应头里的Content-Disposition会影响到小程序端下载后的文件名,建议把日期拼进去,避免同名文件被覆盖。

5.2 小程序端下载 Excel 的两个必填参数

小程序端不能用<a>直接下载,需要通过wx.downloadFile拿到临时文件路径,再用wx.openDocument打开。注意downloadFile的 URL 必须在微信公众平台配置downloadFile合法域名,开发工具里可以临时跳过,真机不行。

wx.downloadFile({ url: 'https://your-domain.com/api/order/export?date=2025-06-01', success: (res) => { if (res.statusCode === 200) { wx.openDocument({ filePath: res.tempFilePath, fileType: 'xlsx', showMenu: true, success: () => { console.log('打开成功'); } }); } } });

fileType要显式传xlsx,否则部分真机无法识别文件格式。showMenu: true可以让用户长按右上角菜单,把文件转发或保存到其他应用。导出日期参数最好用YYYY-MM-DD格式,而不是传时间戳,这样后端 SQL 可以直接按日期范围查询。

5.3 导出前处理好 LocalDateTime 的序列化格式

订单导出最容易被忽略的坑是时间字段。如果后端实体里的createTimeLocalDateTime,在生成 Excel 或返回 JSON 时,没有配置ObjectMapperJavaTimeModule,就会出现一串数字而不是2025-06-01 10:30:00。常见做法是在applicationContext.xmlspring-mvc.xml里给MappingJackson2HttpMessageConverter设置ObjectMapper,并注册JavaTimeModule,指定日期格式为yyyy-MM-dd HH:mm:ss

ObjectMapper objectMapper = new ObjectMapper(); objectMapper.registerModule(new JavaTimeModule()); objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

这个配置不只影响导出,也会影响小程序端setData后显示的下单时间。如果所有接口返回的时间格式都有问题,检查点就在这里,而不是逐个页面去拼接字符串。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询