简介:这是一套面向计算机专业本科生的高分毕业设计级微信小程序水果电商系统,适用于毕设开发、课程设计与期末大作业实战。系统采用Java+SpringBoot构建后端服务,Vue.js开发管理端界面,并通过微信小程序实现用户端轻量化访问,完整覆盖商品展示、购物车、订单管理、用户反馈等核心电商业务流程。资源包共1212个文件,含101个Java后端逻辑文件、127个Vue组件、243个JS交互脚本、63个JSON配置与API响应示例、57个WXSS样式及51个WXML模板,辅以MySQL 5.7+数据库SQL脚本与Navicat建库说明,整体38.87MB,开箱即用。已有163人学习下载,资源经导师验收与多轮调试,包含可直接运行的前后端源码、结构清晰的模块化目录、配套数据库初始化脚本及主流开发工具(IDEA+Maven+Navicat)适配说明,显著降低部署门槛与调试成本。
1. 为什么一个水果店小程序,非得用 SpringBoot + Vue + MySQL 这套组合打底?
你手头这个「基于 Java + SpringBoot + Vue + MySQL 的微信小程序线上水果店系统」,不是玩具 Demo,而是高分毕业设计级别的完整闭环系统——它背后跑的是真实电商逻辑:用户浏览商品、加购下单、微信支付回调、库存扣减、订单状态流转、管理员后台 CRUD。很多人以为“小程序前端 + 云开发”就能搞定,但真到毕业答辩或实习面试时,评委/面试官第一句就问:“库存超卖怎么防?支付成功但订单没生成怎么兜底?Vue 打包后如何和 SpringBoot 静态资源无缝集成?MySQL 的事务隔离级别设成什么才不翻车?”——这些,恰恰是这套技术栈最硬核的落地战场。
它适合三类人:一是计算机专业本科生做毕设,需要可演示、可调试、可讲清技术选型理由的全栈项目;二是刚转行的 Java 或前端新人,想用一个“小而全”的业务场景,把 SpringBoot 的 REST 接口设计、Vue 的路由与状态管理、MySQL 的索引与事务、微信小程序的登录态维护全部串起来练一遍;三是中小团队快速验证水果生鲜类 MVP,因为这套组合没有黑盒依赖,所有模块可控、日志可查、问题可定位。别被“高分毕业设计”几个字唬住——它本质是一套经过教学场景千锤百炼、边界清晰、踩坑路径明确的工程实践模板。
2. 搭建最小可运行环境:从解压 ZIP 到首页能刷出水果列表
拿到微信小程序线上水果店系统.zip后,别急着跑代码。先看清结构:解压后通常有 4 个核心目录——backend-springboot(Java 后端)、frontend-vue(Vue 管理后台)、miniprogram(微信小程序源码)、db(SQL 脚本)。这三端不是孤立的,它们靠约定好的接口协议和数据库 schema 绑定。下面按真实部署顺序操作,跳过所有“理论上应该…”的废话。
2.1 先让 MySQL 数据库活过来:导入 SQL 并确认表结构
进入db目录,你会看到类似fruitshop_init.sql或schema_and_data.sql的文件。这不是简单建库脚本,它包含三部分:CREATE DATABASE、CREATE TABLE(含外键约束)、INSERT INTO(初始化商户、分类、商品、管理员等基础数据)。关键点在于字符集和引擎——必须用utf8mb4和InnoDB,否则微信昵称里的 emoji、用户地址里的生僻字会存乱码甚至报错。
-- 执行前务必确认:MySQL 服务已启动,且你有 root 或足够权限 CREATE DATABASE IF NOT EXISTS fruitshop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE fruitshop; -- 接着 source fruitshop_init.sql提示:如果执行时报
ERROR 1067 (42000): Invalid default value for 'create_time',说明 MySQL 版本 ≥ 5.7 且sql_mode包含NO_ZERO_DATE。临时解决:SET sql_mode=(SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE',''));再重试。长期方案是在my.cnf中配置sql_mode = "STRICT_TRANS_TABLES,NO_ENGINE_SUBSTITUTION"。
导入成功后,用命令行或 Navicat 连上fruitshop库,重点检查三张表:
t_user:字段openid(微信登录唯一标识)、phone(脱敏存储)、status(0=禁用,1=启用)t_order:字段order_no(雪花ID或时间戳+随机数)、pay_status(0=未支付,1=已支付,2=已退款)、update_time(带 ON UPDATE CURRENT_TIMESTAMP)t_product:字段stock(剩余库存)、sales(累计销量)、is_on_sale(是否上架)
这些字段名和状态码,直接决定后续 SpringBoot 接口和 Vue 页面的判读逻辑。别跳过这步——很多“页面空白”问题,根源就是t_product表里没数据,或is_on_sale=0导致查询条件筛空。
2.2 SpringBoot 后端:改对这 3 个配置,8080 端口才能吐出 JSON
打开backend-springboot目录,用 IntelliJ IDEA 或 VS Code 打开(别用记事本!)。核心配置在src/main/resources/application.yml。新手常卡在这三个地方:
- 数据库连接:
url必须带?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai,否则中文存取异常;username/password要和你 MySQL 实例一致(默认常是root/root,但生产环境必须改); - 微信配置:
wechat.appid和wechat.secret是你在微信公众平台申请小程序时获得的,不是公众号的 AppID;wechat.mch_id和wechat.key是微信支付商户号信息,若只做演示可注释掉支付相关 Controller,但登录接口(/api/auth/login)必须有appid和secret才能调用微信code2Session; - 静态资源路径:
spring.resources.static-locations=classpath:/static/,file:./static/—— 这是为了后续把 Vue 打包后的dist目录放进来,让 SpringBoot 直接托管前端资源。
改完保存,右键SpringbootApplication.java→ Run。观察控制台输出:
- 若出现
Tomcat started on port(s): 8080且无Failed to start bean报错,说明启动成功; - 访问
http://localhost:8080/api/product/list,应返回 JSON 数组(如[{"id":1,"name":"赣南脐橙","price":12.5}]); - 若返回 404,检查 Controller 是否加了
@RestController、@RequestMapping("/api"),以及ProductController是否被@ComponentScan扫描到(默认扫描com.fruitshop包下所有类)。
2.3 Vue 管理后台:npm install 后,必须改 proxy 配置才能联调
进入frontend-vue目录,执行npm install(确保 Node.js ≥ 14.x)。安装完成后,关键一步:打开vue.config.js,找到devServer.proxy配置:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 必须指向你的 SpringBoot 地址 changeOrigin: true, pathRewrite: { '^/api': '/api' // 保持前缀不变,SpringBoot 接口就是 /api/xxx } } } } }注意:这里不是写
http://127.0.0.1:8080,也不是http://localhost:8080/api。target只写基础 URL,pathRewrite控制路径映射。很多新手写错导致浏览器 Network 里看到504 Gateway Timeout,其实是 Vue 开发服务器根本没把请求转发出去。
执行npm run serve,访问http://localhost:8080(注意:这是 Vue 的开发端口,和 SpringBoot 的 8080 不冲突),应看到登录页。输入默认账号admin/123456(密码明文存在t_admin表中),进后台能看到商品管理、订单列表。此时打开浏览器开发者工具 → Network,筛选 XHR,刷新页面,确认所有/api/xxx请求状态码都是 200,Response 是有效 JSON。
2.4 微信小程序:用开发者工具打开,填对 appid 才能真机调试
打开微信开发者工具,选择「小程序」→「导入项目」,项目目录选miniprogram。最关键一步:在project.config.json中,把"appid"改为你自己申请的小程序 AppID(格式如wx1234567890abcdef)。如果填错或留空,真机调试时会提示“该小程序未绑定当前微信账号”。
然后检查app.js中的全局配置:
App({ globalData: { baseUrl: 'http://localhost:8080', // 开发时指向本地 SpringBoot // 线上需改为 https://your-domain.com } })再看pages/index/index.js的onLoad方法,它调用getProductListAPI,URL 拼接为${getApp().globalData.baseUrl}/api/product/list。确保这个地址在开发者工具的 Network 面板里能发出请求并返回数据。
提示:若请求失败,先看 Console 是否报
request:fail url not in domain list—— 这是域名未配置。进入微信公众平台 → 开发管理 → 开发设置 → 服务器域名,把localhost加入「request 合法域名」(仅开发阶段允许,上线必须用 HTTPS 域名)。
3. 核心业务链路打通:从微信登录到下单支付的 5 个关键接口
这套系统的价值不在“能跑”,而在“能闭环”。下面拆解最常被答辩老师追问的五个接口,每个都对应一个真实业务动作,也是你复现时最容易卡住的节点。我们不讲理论,只说代码在哪、参数怎么传、返回值怎么看。
3.1 小程序端:wx.login() → 后端 code2Session → 返回自定义 token
微信登录不是直接传用户名密码,而是走标准 OAuth2 流程:
- 小程序调
wx.login()获取临时code; - 前端把
code发给后端/api/auth/login接口; - 后端用
code+appid+secret调用微信https://api.weixin.qq.com/sns/jscode2session; - 微信返回
openid和session_key,后端查库:若openid存在,更新last_login_time;若不存在,插入新用户记录; - 生成 JWT token(含
userid,openid,exp),返回给小程序。
关键代码在backend-springboot/src/main/java/com/fruitshop/controller/AuthController.java:
@PostMapping("/login") public Result login(@RequestBody LoginRequest request) { // 1. 调用微信接口获取 openid String url = "https://api.weixin.qq.com/sns/jscode2session?" + "appid=" + wechatConfig.getAppid() + "&secret=" + wechatConfig.getSecret() + "&js_code=" + request.getCode() + "&grant_type=authorization_code"; String response = restTemplate.getForObject(url, String.class); JSONObject json = JSONObject.parseObject(response); String openid = json.getString("openid"); // 2. 根据 openid 查用户,不存在则创建 User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setStatus(1); userService.save(user); } // 3. 生成 token(实际项目应使用 JJWT 或 Sa-Token) String token = JWT.create() .withClaim("userid", user.getId()) .withClaim("openid", openid) .withExpiresAt(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000)) .sign(Algorithm.HMAC256("your-secret-key")); return Result.success(token); }参数说明:
LoginRequest.code是小程序wx.login()的回调参数;wechatConfig是@ConfigurationProperties("wechat")注入的配置;JWT 秘钥"your-secret-key"必须和application.yml中配置一致,且不能硬编码在代码里,应从环境变量读取。
3.2 商品列表分页:MyBatis-Plus 的 Page 对象怎么用才不漏数据
小程序首页商品列表要求分页、按分类筛选、支持搜索。后端接口/api/product/list接收page,size,categoryId,keyword四个参数。核心是 MyBatis-Plus 的Page<T>分页对象:
@GetMapping("/list") public Result list( @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "10") Integer size, @RequestParam(required = false) Long categoryId, @RequestParam(required = false) String keyword) { Page<Product> productPage = new Page<>(page, size); QueryWrapper<Product> wrapper = new QueryWrapper<>(); wrapper.eq("is_on_sale", 1); // 只查上架商品 if (categoryId != null && categoryId > 0) { wrapper.eq("category_id", categoryId); } if (StringUtils.isNotBlank(keyword)) { wrapper.like("name", keyword).or().like("description", keyword); } Page<Product> result = productService.page(productPage, wrapper); return Result.success(result); }注意:
Page<Product>的泛型必须是实体类Product,不能是Map;wrapper.like("name", keyword)会生成WHERE name LIKE '%keyword%',若数据量大,必须给name字段加FULLTEXT索引或改用 Elasticsearch;result.getRecords()是当前页数据,result.getTotal()是总条数,Vue 前端用这两个值渲染分页器。
3.3 加入购物车:Redis 缓存 vs MySQL 写入,哪个先执行?
购物车功能看似简单,实则涉及并发安全。本系统采用「Redis 存临时购物车 + 下单时落库」策略:
- 小程序调
/api/cart/add,后端解析token得到userid,用userid作为 Redis Key(如cart:123),value 是 JSON 字符串([{"productId":1,"count":2},{"productId":3,"count":1}]); - 下单时
/api/order/create接口,先从 Redis 读购物车,再校验库存(查t_product.stock),扣减成功后写t_order和t_order_item,最后清空 Redis。
关键点在于库存校验必须用数据库行锁:
// 在 OrderService.createOrder() 中 for (CartItem item : cartItems) { // SELECT ... FOR UPDATE 锁住商品行 Product product = productMapper.selectByIdForUpdate(item.getProductId()); if (product.getStock() < item.getCount()) { throw new BusinessException("商品库存不足"); } // 扣减库存(UPDATE t_product SET stock = stock - ? WHERE id = ?) product.setStock(product.getStock() - item.getCount()); productMapper.updateById(product); }提示:
selectByIdForUpdate是 MyBatis-Plus 自定义方法,在ProductMapper.xml中写<select id="selectByIdForUpdate" resultType="Product">SELECT * FROM t_product WHERE id = #{id} FOR UPDATE</select>。没加FOR UPDATE,高并发下会出现超卖。
3.4 微信支付回调:验签、解密、更新订单状态的三步铁律
支付成功后,微信服务器会 POST 数据到你的/api/pay/notify接口。必须严格按微信文档做三件事:
- 验签:用
mch_key对原始 XML 数据排序签名,比对sign字段; - 解密:若
req_info存在(JSAPI 支付返回),用mch_keyAES 解密; - 幂等更新:查订单是否存在、状态是否为“未支付”,再更新为“已支付”。
简化版核心逻辑(真实项目需补全验签和解密):
@PostMapping(value = "/notify", produces = MediaType.APPLICATION_XML_VALUE) public String notify(@RequestBody String xml) { try { Map<String, String> notifyMap = XMLUtil.doXMLParse(xml); String outTradeNo = notifyMap.get("out_trade_no"); // 商户订单号 String resultCode = notifyMap.get("result_code"); if ("SUCCESS".equals(resultCode)) { // 1. 查询订单 Order order = orderService.getByOrderNo(outTradeNo); if (order != null && "0".equals(order.getPayStatus())) { // 2. 更新订单状态 order.setPayStatus("1"); order.setPayTime(new Date()); orderService.updateById(order); // 3. 发送消息(可选:短信、站内信) messageService.sendPaySuccessMessage(order.getUserId(), order.getOrderNo()); } } return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; } catch (Exception e) { log.error("支付回调处理异常", e); return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[系统异常]]></return_msg></xml>"; } }注意:返回 XML 必须是微信指定格式,且
<return_code>为SUCCESS,否则微信会持续重试回调(最长 5 天)。out_trade_no必须和t_order.order_no一致,这是关联支付和订单的唯一凭证。
3.5 管理员后台:Vue Router 的路由守卫如何拦截未登录访问
Vue 管理后台的/admin下所有页面,必须登录后才能访问。实现靠router.beforeEach全局前置守卫:
// router/index.js router.beforeEach((to, from, next) => { const token = localStorage.getItem('admin-token') if (to.meta.requiresAuth && !token) { next('/login') // 跳转登录页 } else if (to.path === '/login' && token) { next('/admin/dashboard') // 已登录还访问登录页,跳转首页 } else { next() } })同时,每个需要权限的页面组件(如ProductList.vue)在mounted钩子中调用权限接口:
mounted() { this.checkPermission() }, methods: { checkPermission() { this.$http.get('/api/admin/permission').then(res => { if (!res.data.permissions.includes('product:list')) { this.$message.error('无权访问') this.$router.push('/403') } }) } }权限数据存在
t_admin_role和t_role_permission关联表中,/api/admin/permission接口返回当前管理员拥有的权限码数组(如["product:add","order:edit"])。Vue 前端用v-if="permissions.includes('product:list')"控制按钮显隐。
4. 避坑指南:这 5 个错误让 80% 的同学毕设答辩当场卡壳
别等答辩被问住才后悔。以下全是我在带学生做毕设时,高频出现、当场翻车、但修复成本极低的坑。每一条都按「现象 → 原因 → 解决」写清楚,照着做就能绕过。
4.1 现象:小程序首页商品列表为空,Network 显示 200 但 Response 是空数组
原因:t_product.is_on_sale字段值为0(下架),而查询 SQL 的WHERE is_on_sale = 1筛掉了所有数据;或者t_category.status = 0导致分类查询返回空,进而商品列表无分类 ID 可传。
解决:用 MySQL 客户端执行SELECT * FROM t_product WHERE is_on_sale = 1 LIMIT 10;,确认有数据;再查SELECT * FROM t_category WHERE status = 1;,确保至少有一个启用的分类。若无,执行UPDATE t_category SET status = 1 WHERE id = 1;。
4.2 现象:Vue 后台登录成功,但点击菜单跳转 404,Console 报NavigationDuplicated
原因:Vue Router 的mode: 'history'在开发环境需配合devServer.historyApiFallback: true,但vue.config.js里没配,或配错位置(应放在devServer对象下,不是configureWebpack)。
解决:在vue.config.js的devServer对象中添加historyApiFallback: true,重启npm run serve。若仍不行,检查路由文件router/index.js中routes数组是否漏写了path: '*'的 404 路由。
4.3 现象:SpringBoot 启动报Caused by: java.lang.ClassNotFoundException: org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
原因:pom.xml中spring-boot-starter-web版本和父 POM 的 SpringBoot 版本不匹配,常见于拷贝旧项目时没更新<parent>的<version>。例如父版本是2.7.18,但spring-boot-starter-web写成了3.1.0。
解决:统一版本。打开pom.xml,确认<parent>的<version>(如2.7.18),然后删掉所有spring-boot-starter-*的<version>标签,让 Maven 继承父 POM 的 BOM 版本管理。
4.4 现象:微信登录返回errcode: 40029, errmsg: invalid code
原因:小程序端wx.login()获取的code未及时传给后端,或后端用错appid/secret(比如用了公众号的,或复制时多了一个空格);更隐蔽的是,code5 分钟内有效,若用户点登录后 10 分钟才提交,微信已失效。
解决:在小程序index.js的login方法里,wx.login()成功后立即调用this.postLogin(code),不要做任何异步等待;后端打印request.getCode()确认长度(正常是 1-time code,约 32 位字符串),再核对application.yml中wechat.appid和wechat.secret是否和微信公众平台完全一致(区分大小写、无空格)。
4.5 现象:下单成功,但t_order表里pay_status = 0,且t_product.stock没扣减
原因:OrderService.createOrder()方法没加@Transactional(rollbackFor = Exception.class)注解,导致数据库操作中途异常(如库存不足)时,前面的INSERT INTO t_order已提交,无法回滚。
解决:在OrderService的createOrder方法上添加@Transactional,并确认类上没加@Transactional(propagation = Propagation.NOT_SUPPORTED)这类覆盖性注解;同时检查application.yml中spring.datasource.hikari.transaction-isolation是否为默认值(TRANSACTION_REPEATABLE_READ即可)。
5. 进阶技巧:把 Vue 打包放进 SpringBoot,实现真正的前后端一体部署
毕设演示或小团队上线,没人愿意同时维护两个服务器(Nginx 托管 Vue + Tomcat 托管 SpringBoot)。最佳实践是:npm run build生成dist目录,把它扔进 SpringBoot 的src/main/resources/static,让 SpringBoot 一并托管。这样只需部署一个 JAR 包,访问http://your-server:8080就是 Vue 后台,http://your-server:8080/api/xxx就是接口。但直接扔进去会 404——因为 Vue Router 的history模式需要服务端兜底。
5.1 步骤一:Vue 打包前,配置 publicPath 和 outputDir
修改frontend-vue/vue.config.js:
module.exports = { // 静态资源基础路径,必须以 / 开头,且和 SpringBoot 的 context-path 一致 publicPath: '/', // 打包输出目录,要和 SpringBoot 的 static 目录对齐 outputDir: '../backend-springboot/src/main/resources/static', // 关键:关闭 CSS 提取,避免打包后样式丢失 css: { extract: false } }注意:
outputDir指向backend-springboot的static目录,确保路径正确(可用相对路径../backend-springboot/...)。publicPath: '/'表示所有资源(JS/CSS/IMG)都从根路径加载,和 SpringBoot 默认 context-path 一致。
5.2 步骤二:SpringBoot 添加兜底路由,让 Vue Router 正常工作
在backend-springboot的任意@Configuration类中,添加WebMvcConfigurer:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 静态资源优先匹配 registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); } @Override public void addViewControllers(ViewControllerRegistry registry) { // 所有非 API 请求,都交给 index.html 处理(Vue Router 的 history 模式兜底) registry.addViewController("/").setViewName("forward:/index.html"); registry.addViewController("/admin/**").setViewName("forward:/index.html"); registry.addViewController("/login").setViewName("forward:/index.html"); } }逻辑说明:当请求
/admin/dashboard时,SpringBoot 先查static/admin/dashboard.html(不存在),再匹配ViewController规则,转发到/index.html;Vue Router 拿到 URL 后,根据router.push('/admin/dashboard')渲染对应组件。这样就实现了单页应用的 URL 路由。
5.3 步骤三:调整 Axios 基础 URL,适配打包后路径
frontend-vue/src/utils/request.js中,baseURL不能写死http://localhost:8080,否则打包后请求会发到开发机:
// 开发环境用代理,生产环境用相对路径 const service = axios.create({ baseURL: process.env.NODE_ENV === 'production' ? '/' : '/api', timeout: 10000 })同时,在backend-springboot的application.yml中,确保server.servlet.context-path为空(即/),否则所有接口要加前缀:
server: port: 8080 servlet: context-path: "/" # 必须是 "/",不能是 "/api"5.4 验证:打包、启动、测试全流程
- 进入
frontend-vue目录,执行npm run build,确认backend-springboot/src/main/resources/static下生成了index.html、js/、css/等文件; - 进入
backend-springboot目录,执行mvn clean package -Dmaven.test.skip=true,生成target/fruitshop-1.0.jar; - 执行
java -jar target/fruitshop-1.0.jar,观察日志直到Started SpringbootApplication; - 访问
http://localhost:8080,应看到 Vue 登录页;登录后,F12 → Network,确认所有请求 URL 都是http://localhost:8080/api/xxx,状态码 200; - 修改
t_product表数据,刷新页面,确认列表实时更新——证明静态资源和接口共用一个端口,且数据联动正常。
血泪经验:第一次打包后访问白屏,90% 是
index.html里的 JS/CSS 路径错了。打开浏览器开发者工具 → Network → Filterjs,看.js文件是否 404。若是,检查vue.config.js的publicPath是否为'/',以及index.html源码中<script src="/js/app.xxx.js">的路径是否以/开头。
我带过的 37 个毕设学生里,有 22 个卡在“Vue 打包后接口 404”,最后发现全是publicPath没设对,或者baseURL写死了localhost。真正花时间的从来不是写代码,而是理解路径怎么流转、谁在什么时候解析 URL。希望帮到你。
本文还有配套的精品资源,点击获取