SpringBoot+Vue+MySQL水果店系统搭建与避坑指南

发布时间:2026/10/7 16:37:52
SpringBoot+Vue+MySQL水果店系统搭建与避坑指南 简介这是一套面向计算机专业本科生的高分毕业设计级微信小程序水果电商系统适用于毕设开发、课程设计与期末大作业实战。系统采用JavaSpringBoot构建后端服务Vue.js开发管理端界面并通过微信小程序实现用户端轻量化访问完整覆盖商品展示、购物车、订单管理、用户反馈等核心电商业务流程。资源包共1212个文件含101个Java后端逻辑文件、127个Vue组件、243个JS交互脚本、63个JSON配置与API响应示例、57个WXSS样式及51个WXML模板辅以MySQL 5.7数据库SQL脚本与Navicat建库说明整体38.87MB开箱即用。已有163人学习下载资源经导师验收与多轮调试包含可直接运行的前后端源码、结构清晰的模块化目录、配套数据库初始化脚本及主流开发工具IDEAMavenNavicat适配说明显著降低部署门槛与调试成本。1. 为什么一个水果店小程序非得用 SpringBoot Vue MySQL 这套组合打底你手头这个「基于 Java SpringBoot Vue MySQL 的微信小程序线上水果店系统」不是玩具 Demo而是高分毕业设计级别的完整闭环系统——它背后跑的是真实电商逻辑用户浏览商品、加购下单、微信支付回调、库存扣减、订单状态流转、管理员后台 CRUD。很多人以为“小程序前端 云开发”就能搞定但真到毕业答辩或实习面试时评委/面试官第一句就问“库存超卖怎么防支付成功但订单没生成怎么兜底Vue 打包后如何和 SpringBoot 静态资源无缝集成MySQL 的事务隔离级别设成什么才不翻车”——这些恰恰是这套技术栈最硬核的落地战场。它适合三类人一是计算机专业本科生做毕设需要可演示、可调试、可讲清技术选型理由的全栈项目二是刚转行的 Java 或前端新人想用一个“小而全”的业务场景把 SpringBoot 的 REST 接口设计、Vue 的路由与状态管理、MySQL 的索引与事务、微信小程序的登录态维护全部串起来练一遍三是中小团队快速验证水果生鲜类 MVP因为这套组合没有黑盒依赖所有模块可控、日志可查、问题可定位。别被“高分毕业设计”几个字唬住——它本质是一套经过教学场景千锤百炼、边界清晰、踩坑路径明确的工程实践模板。2. 搭建最小可运行环境从解压 ZIP 到首页能刷出水果列表拿到微信小程序线上水果店系统.zip后别急着跑代码。先看清结构解压后通常有 4 个核心目录——backend-springbootJava 后端、frontend-vueVue 管理后台、miniprogram微信小程序源码、dbSQL 脚本。这三端不是孤立的它们靠约定好的接口协议和数据库 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脱敏存储、status0禁用,1启用t_order字段order_no雪花ID或时间戳随机数、pay_status0未支付,1已支付,2已退款、update_time带 ON UPDATE CURRENT_TIMESTAMPt_product字段stock剩余库存、sales累计销量、is_on_sale是否上架这些字段名和状态码直接决定后续 SpringBoot 接口和 Vue 页面的判读逻辑。别跳过这步——很多“页面空白”问题根源就是t_product表里没数据或is_on_sale0导致查询条件筛空。2.2 SpringBoot 后端改对这 3 个配置8080 端口才能吐出 JSON打开backend-springboot目录用 IntelliJ IDEA 或 VS Code 打开别用记事本。核心配置在src/main/resources/application.yml。新手常卡在这三个地方数据库连接url必须带?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai否则中文存取异常username/password要和你 MySQL 实例一致默认常是root/root但生产环境必须改微信配置wechat.appid和wechat.secret是你在微信公众平台申请小程序时获得的不是公众号的 AppIDwechat.mch_id和wechat.key是微信支付商户号信息若只做演示可注释掉支付相关 Controller但登录接口/api/auth/login必须有appid和secret才能调用微信code2Session静态资源路径spring.resources.static-locationsclasspath:/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只写基础 URLpathRewrite控制路径映射。很多新手写错导致浏览器 Network 里看到504 Gateway Timeout其实是 Vue 开发服务器根本没把请求转发出去。执行npm run serve访问http://localhost:8080注意这是 Vue 的开发端口和 SpringBoot 的 8080 不冲突应看到登录页。输入默认账号admin/123456密码明文存在t_admin表中进后台能看到商品管理、订单列表。此时打开浏览器开发者工具 → Network筛选 XHR刷新页面确认所有/api/xxx请求状态码都是 200Response 是有效 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方法它调用getProductListAPIURL 拼接为${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接口后端用codeappidsecret调用微信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.javaPostMapping(/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_typeauthorization_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 的PageT分页对象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) { PageProduct productPage new Page(page, size); QueryWrapperProduct 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); } PageProduct result productService.page(productPage, wrapper); return Result.success(result); }注意PageProduct的泛型必须是实体类Product不能是Mapwrapper.like(name, keyword)会生成WHERE name LIKE %keyword%若数据量大必须给name字段加FULLTEXT索引或改用 Elasticsearchresult.getRecords()是当前页数据result.getTotal()是总条数Vue 前端用这两个值渲染分页器。3.3 加入购物车Redis 缓存 vs MySQL 写入哪个先执行购物车功能看似简单实则涉及并发安全。本系统采用「Redis 存临时购物车 下单时落库」策略小程序调/api/cart/add后端解析token得到userid用userid作为 Redis Key如cart:123value 是 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 idselectByIdForUpdate resultTypeProductSELECT * 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 { MapString, 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 xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; } catch (Exception e) { log.error(支付回调处理异常, e); return xmlreturn_code![CDATA[FAIL]]/return_codereturn_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-ifpermissions.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 后台登录成功但点击菜单跳转 404Console 报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.jsmodule.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类中添加WebMvcConfigurerConfiguration 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.htmlVue 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: / # 必须是 /不能是 /api5.4 验证打包、启动、测试全流程进入frontend-vue目录执行npm run build确认backend-springboot/src/main/resources/static下生成了index.html、js/、css/等文件进入backend-springboot目录执行mvn clean package -Dmaven.test.skiptrue生成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。希望帮到你。本文还有配套的精品资源点击获取