简介:这是一套完整的微信小程序农产品销售平台实战项目源码,面向前端与全栈开发者,尤其适合学习小程序+Java后端(SSM框架)+MySQL数据库协同开发的中初级工程师。项目涵盖用户端与管理后台双视角功能,用户可完成注册登录、浏览首页/农产品/个人中心;管理员可进行农产品、分类、订单、用户等10余类模块的全流程CRUD操作,具备真实电商系统典型架构特征。资源包共1195个文件,以166个JS逻辑文件、133个Vue组件、127个Java后端代码、231个PNG/SVG图标及80个WXML/WXSS页面样式文件为主,辅以SQL脚本、配置文件与批处理脚本(如install.bat/run.bat),结构清晰、开箱即用,压缩包仅14.86MB。已有488人学习下载,提供完整可运行工程、模块化目录结构、前后端分离实现范例及常见微信小程序调试排错参考,是快速掌握小程序商业项目落地的优质实践样本。
1. 这不是又一个“Hello World”小程序:一个能真上架、真走通订单闭环的农产品销售平台源码,为什么值得你花20分钟拆开看?
你见过太多“微信小程序源码”——点开是空荡荡的app.js、三行console.log、首页只有一张轮播图加两行文字,连登录按钮都绑不上事件。但这份微信小程序开发项目实例-农产品销售平台(源码).zip不是Demo,是实打实跑在微信开发者工具里、后端用SSM(Spring+SpringMVC+MyBatis)搭在本地Tomcat、MySQL存着真实商品和订单数据的完整业务系统。它不教你怎么写wx.showToast,而是直接给你一套「用户扫码进店→浏览分类→加购→微信支付→管理员后台发货→订单状态自动流转」的全链路代码骨架。尤其适合两类人:一是Java后端想补全小程序前端能力的工程师,二是高校课程设计/毕设需要可演示、可答辩、可改写、不翻车的实战项目。它没用uni-app或Taro,就是原生WXML+WXSS+JS,所有接口调用、token管理、分页逻辑、表单校验都写死在.vue文件里(注意:这是Vue风格的WXML结构,非uni-app,但命名和组织方式高度接近),意味着你能一眼看清微信小程序与Java后端如何通过RESTful API咬合。别被.bak文件吓退——那是开发者留下的历史快照,不是残缺备份。
2. 拆包即运行:从解压到首页渲染的5步落地流程(含.bat脚本真相解析)
2.1 解压后第一眼该盯什么?识别项目分层结构
解压微信小程序开发项目实例-农产品销售平台(源码).zip后,你会看到混合目录:既有main.css.bak、update-password.vue.bak这类带.bak后缀的文件,也有3-build.bat、2-run.bat、1-install.bat三个批处理脚本,还有.classpath和org.eclipse.wst.common.component这种Eclipse专属配置文件。这说明:这不是纯前端项目,而是Java Web工程 + 小程序前端的混合体,且开发环境基于Eclipse + Tomcat。
关键目录结构如下(手动整理,非IDE自动生成):
| 目录/文件名 | 类型 | 作用 | 是否必须 |
|---|---|---|---|
src/main/webapp/ | Java Web根路径 | 存放小程序前端静态资源(WXML/WXSS/JS)、后端JSP页面、静态HTML | ✅ 必须 |
src/main/java/ | Java源码 | SSM三层结构:controller(接口层)、service(业务逻辑)、mapper(数据库操作) | ✅ 必须 |
src/main/resources/ | 配置文件 | spring-context.xml、spring-mvc.xml、mybatis-config.xml、jdbc.properties | ✅ 必须 |
WebContent/或webapp/ | 前端资源入口 | 小程序project.config.json所在目录,也是微信开发者工具导入路径 | ✅ 必须 |
3-build.bat | 构建脚本 | 执行mvn clean package -Dmaven.test.skip=true,生成WAR包 | ⚠️ 可跳过(新手建议先手动部署) |
2-run.bat | 启动脚本 | 调用startup.bat启动Tomcat,并部署WAR包 | ⚠️ 可跳过(新手建议用IDE直启) |
提示:
.bak文件(如IndexHeader.vue.bak)是开发者用Eclipse保存的旧版本快照,不是损坏文件,也不是必须删除。它们的存在恰恰证明这个项目经历过多次迭代,比如update-password.vue.bak很可能对应密码修改功能的V1版,而当前生效的是无后缀的update-password.vue。初学者可先忽略所有.bak,专注无后缀文件。
2.2 后端启动:用Eclipse直启Tomcat,绕过.bat脚本的玄学依赖
很多新手卡在2-run.bat报错:“找不到JAVA_HOME”、“'mvn' 不是内部命令”。这不是源码问题,而是环境变量未配齐。我一般会跳过所有.bat,用Eclipse直启——因为项目自带.project和.classpath,Eclipse能自动识别为Dynamic Web Project。
操作步骤:
- 确保已安装 JDK 8(必须!SSM老项目普遍不兼容JDK 17+)和 Eclipse IDE for Enterprise Java Developers(2021-06或更新版);
- Eclipse → File → Import → Existing Projects into Workspace → 选择解压后的根目录 → Finish;
- 右键项目 → Properties → Targeted Runtimes → 勾选已配置的 Apache Tomcat v8.5(必须v8.5,v9+有Servlet 4.0兼容问题);
- 右键项目 → Run As → Run on Server → 选择Tomcat v8.5 → Finish;
- 控制台输出
INFO: Server startup in [xxx] ms后,在浏览器访问http://localhost:8080/—— 应看到Tomcat欢迎页,说明服务已起;再访问http://localhost:8080/manager/html(需配置Tomcat用户权限)可确认应用是否部署成功。
参数说明:
jdbc.properties中默认数据库地址为jdbc:mysql://localhost:3306/agriculture?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8,用户名root,密码为空。若你的MySQL端口不是3306或密码非空,必须修改此处,否则后端启动时会报Communications link failure。这是新手踩坑率最高的点——不是代码错,是数据库连不上。
2.3 小程序前端导入:微信开发者工具认准project.config.json,而非app.js
微信开发者工具不能直接打开src/main/webapp/目录,因为那只是Java Web项目的静态资源存放位置。小程序真正的根目录是src/main/webapp/下的子目录,通常叫miniprogram/或weapp/。但本项目没显式创建该目录——它的WXML/WXSS/JS文件就散落在src/main/webapp/下,且project.config.json就在此层级。
验证方法:
在src/main/webapp/目录下执行ls -la(Mac/Linux)或dir(Windows),你会看到:
project.config.json app.js app.json app.wxss pages/ utils/这说明:src/main/webapp/就是小程序项目根目录。微信开发者工具导入时,路径必须选到这里,而不是整个ZIP解压根目录。
导入步骤:
- 打开微信开发者工具 → 新建项目 → 选择
src/main/webapp/目录; - AppID填
*(测试号); - 项目名称随意;
- 勾选“在新窗口打开”;
- 点击“确定”,等待编译完成。
首次编译可能报Cannot find module 'utils/util.js',这是因为utils/util.js里引用了require('util'),而微信小程序环境没有Node.js的util模块。解决方法:注释掉utils/util.js第1行const util = require('util');,并删除所有util.format()调用,改用字符串模板。这是本项目最典型的环境适配坑,后面章节详述。
2.4 接口联调:前端如何调用Java后端?看懂app.js里的全局 baseURL
小程序前端所有网络请求,都通过app.js中定义的全局baseURL发起。打开src/main/webapp/app.js,找到:
App({ globalData: { userInfo: null, baseURL: 'http://localhost:8080/agriculture/' // ← 关键!必须与后端Context Path一致 } })注意agriculture是Tomcat部署的应用名(Context Path)。你在Eclipse中右键项目 → Properties → Web Project Settings → Context root,确认值为agriculture。若不同,必须同步修改baseURL,否则所有请求返回404。
再看首页商品列表请求(pages/index/index.js):
wx.request({ url: getApp().globalData.baseURL + 'product/list', // 实际请求 http://localhost:8080/agriculture/product/list method: 'GET', success: (res) => { this.setData({ productList: res.data }) } })这就是SSM后端Controller的映射路径:
// src/main/java/com/agriculture/controller/ProductController.java @RestController @RequestMapping("/product") public class ProductController { @GetMapping("/list") public List<Product> list() { ... } }前后端路径严格对应,没有网关、没有Nginx代理、没有跨域配置——因为同域(localhost:8080)。这是本地开发的最大便利,也是上线前必须改造的点(需加反向代理或CORS)。
3. 功能模块逐个击穿:注册登录、商品管理、订单闭环,代码在哪、怎么改?
3.1 用户注册与登录:JWT Token如何存、取、验?看login.vue和拦截器
用户登录不是简单发个POST,而是涉及Token生成、存储、请求头携带、后端校验四步闭环。本项目用JWT(JSON Web Token),但实现极简——Token存在wx.setStorageSync('token', res.data.token),每次请求从wx.getStorageSync('token')取出,拼在Header里。
打开pages/login/login.vue(注意:这是Vue语法写的WXML组件,非标准Vue项目,但结构一致):
<!-- login.vue --> <view class="btn-login" bindtap="doLogin">登录</view>// login.vue 的js部分 doLogin() { wx.request({ url: getApp().globalData.baseURL + 'user/login', method: 'POST', data: { username: this.data.username, password: this.data.password }, success: (res) => { if (res.data.code === 200) { wx.setStorageSync('token', res.data.token) // ← Token存本地 wx.switchTab({ url: '/pages/index/index' }) } } }) }后端UserController.login()方法生成Token:
// UserController.java @PostMapping("/login") public Result login(@RequestBody User user) { User dbUser = userService.findByUsername(user.getUsername()); if (dbUser != null && dbUser.getPassword().equals(user.getPassword())) { String token = JWTUtil.sign(dbUser.getId(), dbUser.getUsername()); // 使用jjwt库生成 return Result.success(token); } return Result.error("用户名或密码错误"); }关键拦截器JwtInterceptor.java在src/main/java/com/agriculture/interceptor/下:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("Authorization"); // ← 前端必须传Header if (token == null || !token.startsWith("Bearer ")) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token缺失"); return false; } try { Jwts.parser().setSigningKey("agriculture-secret").parseClaimsJws(token.substring(7)); } catch (Exception e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token无效"); return false; } return true; }注意:
"agriculture-secret"是硬编码密钥,上线前必须替换成环境变量或配置中心读取。当前代码里所有/admin/**路径都注册了此拦截器,意味着管理员接口也走JWT校验——但实际登录态管理是分离的(管理员用Session),这点设计有矛盾,后面避坑章会讲。
3.2 农产品管理后台:SSM三层如何联动?从ProductController到ProductMapper.xml
管理员登录后进入pages/admin/product/product-list.vue,点击“新增”调用addProduct():
addProduct() { wx.navigateTo({ url: '/pages/admin/product/product-add.vue' }) }product-add.vue提交表单:
wx.request({ url: getApp().globalData.baseURL + 'admin/product/add', method: 'POST', data: this.data.formData, header: { 'Authorization': 'Bearer ' + wx.getStorageSync('token') }, // ← 携带Token success: () => wx.navigateBack() })后端AdminProductController.add():
@PostMapping("/admin/product/add") public Result add(@RequestBody Product product) { productService.add(product); // ← Service层 return Result.success(); }ProductService.add():
@Transactional public void add(Product product) { productMapper.insert(product); // ← Mapper层,操作数据库 }ProductMapper.xml(src/main/resources/mapper/ProductMapper.xml):
<insert id="insert" parameterType="com.agriculture.entity.Product"> INSERT INTO product (name, price, category_id, image_url, description) VALUES (#{name}, #{price}, #{categoryId}, #{imageUrl}, #{description}) </insert>这就是SSM经典三层:Controller接收请求 → Service编排业务(含事务)→ Mapper执行SQL。所有实体类(Product.java)、Mapper接口(ProductMapper.java)、XML映射文件一一对应,字段名完全一致。若你要新增“产地”字段,只需三步:①Product.java加private String origin;②ProductMapper.xml<insert>SQL里加origin③product-add.vue表单加输入框并绑定formData.origin。
3.3 订单全流程:从购物车提交到状态变更,状态机如何用数据库字段驱动?
订单不是单次请求,而是状态流转:待支付 → 已支付 → 已发货 → 已签收。本项目用order_status字段(tinyint)驱动,值为0/1/2/3。
用户下单(pages/cart/cart.vue):
submitOrder() { wx.request({ url: getApp().globalData.baseURL + 'order/submit', method: 'POST', data: { cartItems: this.data.cartItems }, success: (res) => { if (res.data.code === 200) { // 调用微信支付API(此处省略具体调用,实际需后端统一下单) wx.requestPayment({ /* 支付参数 */ }) } } }) }后端OrderController.submit()创建订单:
@PostMapping("/order/submit") public Result submit(@RequestBody OrderSubmitDTO dto) { Order order = new Order(); order.setUserId(getCurrentUserId()); // 从JWT解析 order.setOrderStatus(0); // ← 初始状态:待支付 order.setTotalPrice(dto.getTotalPrice()); orderService.create(order); return Result.success(order.getId()); }管理员发货(pages/admin/order/order-list.vue):
shipOrder(id) { wx.request({ url: getApp().globalData.baseURL + 'admin/order/ship', method: 'POST', data: { id: id }, success: () => this.loadOrders() // 刷新列表 }) }AdminOrderController.ship():
@PostMapping("/admin/order/ship") public Result ship(@RequestBody Map<String, Long> params) { Order order = orderService.findById(params.get("id")); order.setOrderStatus(2); // ← 直接设为已发货 orderService.update(order); return Result.success(); }没有状态机引擎,全靠if-else和字段值判断。例如订单详情页(pages/order/detail.vue)根据order_status渲染不同按钮:
<view wx:if="{{order.orderStatus == 0}}"> <button bindtap="payOrder">去支付</button> </view> <view wx:if="{{order.orderStatus == 1}}"> <button bindtap="confirmReceive">确认收货</button> </view>这种设计简单直接,适合中小项目。若未来要加“取消订单”、“申请售后”,只需扩展order_status值和对应前端逻辑。
4. 避坑指南:5个血泪经验总结,避开90%新手翻车现场
4.1 现象:微信开发者工具编译报错Cannot find module 'utils/util.js'
原因:utils/util.js第1行const util = require('util');尝试引入Node.js内置模块,但小程序环境无此模块。
解决:打开src/main/webapp/utils/util.js,注释第1行// const util = require('util');,并将所有util.format()替换为ES6模板字符串。例如:
// 原代码(报错) console.log(util.format('用户%s登录失败', username)); // 改为(正常) console.log(`用户${username}登录失败`);4.2 现象:Eclipse启动Tomcat后,访问http://localhost:8080/agriculture/返回404
原因:项目未正确部署到Tomcat,或Context Path不匹配。
解决:
- Eclipse中右键项目 → Properties → Web Project Settings → 确认 Context root 为
agriculture; - 右键项目 → Run As → Run on Server → 确保勾选 “Always use this server configuration”;
- 启动后,打开
http://localhost:8080/manager/html,在“Applications”列表中查找agriculture,若状态为stopped,点击Start; - 若仍404,检查
src/main/webapp/WEB-INF/web.xml中<welcome-file-list>是否包含index.jsp,且该文件存在。
4.3 现象:登录成功后跳转首页,但商品列表空白,控制台报GET http://localhost:8080/agriculture/product/list 401
原因:前端未在请求Header中携带Token,或后端JWT拦截器未放行/product/list路径。
解决:
- 检查
pages/index/index.js中wx.request是否添加header: { 'Authorization': 'Bearer ' + wx.getStorageSync('token') }; - 检查
JwtInterceptor.java的preHandle方法,确认request.getRequestURI()包含/product/list时未被return false拦截; - 关键:
/product/list是用户接口,不应被JWT拦截器拦截(因用户登录态用Token,管理员用Session),需在拦截器中加白名单:
if (request.getRequestURI().startsWith("/product/") || request.getRequestURI().startsWith("/user/")) { return true; // 放行用户相关接口 }4.4 现象:MySQL插入中文乱码,商品名显示为????
原因:MySQL连接URL未指定UTF-8编码,或数据库/表字符集非utf8mb4。
解决:
- 修改
src/main/resources/jdbc.properties:jdbc.url=jdbc:mysql://localhost:3306/agriculture?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8&allowPublicKeyRetrieval=true&useSSL=false - 登录MySQL,执行:
ALTER DATABASE agriculture CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
4.5 现象:管理员登录后,点击“农产品管理”报403 Forbidden
原因:管理员接口(/admin/**)使用Session校验,但前端未发送JSESSIONID Cookie。
解决:
- 微信小程序默认不发送Cookie,需显式开启
withCredentials: true; - 修改所有管理员接口请求(如
pages/admin/product/product-list.vue):wx.request({ url: getApp().globalData.baseURL + 'admin/product/list', method: 'GET', withCredentials: true, // ← 关键!让小程序发送Cookie success: (res) => { ... } }) - 后端
WebConfig.java需配置CORS允许凭据:@Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("https://devtools.qyapi.weixin.qq.com")); configuration.setAllowCredentials(true); // ← 允许Cookie configuration.addAllowedMethod("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; }
5. 进阶技巧:把这套源码改造成你的课程设计/毕设项目(含3个可立即落地的改造点)
5.1 改造点一:替换微信支付为模拟支付,绕过企业资质审核
微信小程序真实支付需企业认证+微信支付商户号,学生项目根本搞不定。本项目pages/order/submit.vue里wx.requestPayment()是占位符,直接删掉,改成模拟支付成功逻辑:
// pages/order/submit.vue payOrder() { // 删除 wx.requestPayment({...}) 整段 wx.request({ url: getApp().globalData.baseURL + 'order/pay', method: 'POST', data: { orderId: this.data.orderId }, success: (res) => { if (res.data.code === 200) { wx.showToast({ title: '支付成功', icon: 'success' }); setTimeout(() => wx.switchTab({ url: '/pages/order/order-list' }), 1500); } } }) }后端OrderController.pay():
@PostMapping("/order/pay") public Result pay(@RequestBody Map<String, Long> params) { Order order = orderService.findById(params.get("orderId")); order.setOrderStatus(1); // ← 设为已支付 orderService.update(order); return Result.success(); }这样,点击“去支付”直接跳转订单列表,状态变为“已支付”。答辩时演示流畅,且代码改动仅3处,零风险。
5.2 改造点二:增加图片上传功能,用本地路径替代七牛云
原项目商品图用绝对路径http://localhost:8080/upload/xxx.jpg,但upload目录不在项目内。用小程序自带wx.chooseImage+wx.uploadFile上传到后端,再存入src/main/webapp/upload/:
- 前端
pages/admin/product/product-add.vue加按钮:<button bindtap="chooseImage">选择图片</button> <image src="{{imageUrl}}" mode="aspectFill" /> - JS中:
chooseImage() { wx.chooseImage({ count: 1, success: (res) => { const tempFilePath = res.tempFilePaths[0]; wx.uploadFile({ url: getApp().globalData.baseURL + 'admin/upload', filePath: tempFilePath, name: 'file', success: (uploadRes) => { const data = JSON.parse(uploadRes.data); this.setData({ imageUrl: data.url }); // data.url 如 /upload/20231001123456.jpg } }) } }) } - 后端
UploadController.java:@PostMapping("/admin/upload") public Result upload(@RequestParam("file") MultipartFile file) throws IOException { String fileName = System.currentTimeMillis() + "_" + file.getOriginalFilename(); String uploadPath = "src/main/webapp/upload/"; new File(uploadPath).mkdirs(); file.transferTo(new File(uploadPath + fileName)); return Result.success("/upload/" + fileName); // 返回相对路径供前端显示 }
5.3 改造点三:导出订单Excel,用Apache POI一行代码搞定
毕设常要求“数据导出”。本项目订单列表只有展示,加一个导出按钮,后端用POI生成Excel:
pom.xml加依赖:<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>4.1.2</version> </dependency>AdminOrderController.java加方法:@GetMapping("/admin/order/export") public void export(HttpServletResponse response) throws IOException { List<Order> orders = orderService.findAll(); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("订单列表"); // 写表头... for (int i = 0; i < orders.size(); i++) { XSSFRow row = sheet.createRow(i + 1); row.createCell(0).setCellValue(orders.get(i).getId()); row.createCell(1).setCellValue(orders.get(i).getTotalPrice()); // ...其他字段 } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=orders.xlsx"); workbook.write(response.getOutputStream()); }- 前端
pages/admin/order/order-list.vue加按钮:<button bindtap="exportOrders">导出Excel</button>exportOrders() { wx.downloadFile({ url: getApp().globalData.baseURL + 'admin/order/export', success: (res) => { if (res.statusCode === 200) { wx.openDocument({ filePath: res.tempFilePath, success: () => console.log('打开成功') }) } } }) }
从那以后我每次接手课程设计源码,都强制走一遍这三步:① 把支付改成模拟逻辑,确保演示不卡壳;② 把图片上传路径本地化,避免七牛云配置黑洞;③ 给核心列表加导出功能,答辩时老师问“数据怎么分析”,直接点按钮弹Excel。这三步做完,项目就从“能跑”变成“能讲、能演、能交”,比堆砌炫酷动画实在得多。希望帮到你。
本文还有配套的精品资源,点击获取