资讯动态

SpringBoot+Vue3+小程序生鲜系统实战指南

发布时间:2026/9/16 18:07:49 来源:尧图企业网站定制
简介这是一套基于SpringBootVue3微信小程序技术栈开发的生鲜订购系统完整项目面向高校计算机专业学生及Java/前端初学者适用于课程设计、期末大作业与毕业设计选题。资源包含前后端分离的全部源码124个Java后端文件、155个Vue组件、274个JS逻辑文件、可直接执行的SQL建库脚本、微信小程序WXML/WXSS/WXS页面文件共67732个以及配套图标、样式、配置等资源总计1357个文件压缩包仅12.2MB轻量易部署。已有54人学习下载项目经导师指导并高分通过所有代码均通过严格调试下载解压后按文档说明即可本地运行。附带系统演示视频含登录、商品浏览、下单、订单管理等核心流程并提供清晰的目录结构与多环境配置development/production/yml便于理解前后端协作机制与小程序开发规范。1. 为什么生鲜订购系统要选 SpringBoot Vue3 微信小程序这个技术栈你刚接手一个社区团购类项目老板说“要快上线、能改价格、支持秒杀、微信里直接下单”但团队只有 2 个后端、1 个前端、1 个小程序开发者——这种现实场景下A119 这套基于 SpringBoot Vue3 微信小程序的生鲜订购系统不是炫技的 Demo而是经过真实业务验证的最小可行技术组合。它用 SpringBoot 快速构建 RESTful 接口层商品管理、订单状态机、库存扣减、微信支付回调用 Vue3 管理后台做运营侧数据看板与 SKU 批量操作用微信小程序承载 C 端高频交互首页轮播分类导航购物车本地缓存地址选择支付跳转。三者通过标准 HTTP 协议通信前后端分离清晰接口契约明确连数据库脚本都已按 MySQL 8.0 兼容格式导出含建表语句、初始品类数据、管理员账号。这不是“学完就能做”的教学项目而是“拉下来改改就能跑”的生产级骨架比如商品列表页默认加载 10 条但你只需改PageHelper.startPage(1, 20)就能适配大屏终端比如微信登录态校验逻辑封装在WxLoginInterceptor替换为手机号验证码只需重写该拦截器的preHandle方法。适合中小团队快速交付、后续可横向扩展为多端H5/APP的起点。2. 后端 SpringBoot 模块拆解与关键配置落地SpringBoot 作为后端核心其价值不在于“自动装配”而在于对生鲜业务高频场景的精准抽象。A119 项目采用典型的分层结构controller → service → mapper → entity但真正决定交付效率的是三个被显式强化的模块——库存事务控制、微信支付回调路由、以及跨域与 Token 安全策略。这些不是靠文档说明而是靠代码里的注解和配置文件直接体现。2.1 库存扣减必须保证原子性Transactional 乐观锁双保险生鲜商品库存变更频繁且不允许超卖。项目未使用 Redis 分布式锁增加运维复杂度而是采用数据库层面的乐观锁机制在product表中增加version字段并在更新 SQL 中强制校验// ProductMapper.xml update iddecreaseStock parameterTypemap UPDATE product SET stock stock - #{count}, version version 1 WHERE id #{id} AND version #{version} /update对应 Service 层方法需标注Transactional并捕获更新失败异常Transactional(rollbackFor Exception.class) public boolean reduceStock(Long productId, Integer count) { Product product productMapper.selectById(productId); if (product.getStock() count) { throw new BusinessException(库存不足); } int updated productMapper.decreaseStock(Map.of(id, productId, count, count, version, product.getVersion())); if (updated 0) { // version 不匹配说明并发修改发生重试或抛异常 throw new BusinessException(库存更新冲突请重试); } return true; }提示Transactional默认只对 unchecked exceptionRuntimeException 及其子类回滚。此处BusinessException继承自RuntimeException确保库存扣减失败时订单创建事务整体回滚。2.2 微信支付回调必须严格校验签名与订单状态微信支付回调接口/api/pay/notify是高危入口项目通过WXPayUtil工具类完成三重校验① 解析 XML 并验证return_code和result_code均为 SUCCESS② 使用商户 API 密钥重新生成sign并比对③ 查询本地订单状态仅当订单为“待支付”时才执行状态更新。关键代码如下PostMapping(/notify) public String handleNotify(HttpServletRequest request) throws IOException { String xmlResult IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); MapString, String notifyMap WXPayUtil.xmlToMap(xmlResult); // 1. 基础状态校验 if (!SUCCESS.equals(notifyMap.get(return_code)) || !SUCCESS.equals(notifyMap.get(result_code))) { return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[签名校验失败]]/return_msg/xml; } // 2. 签名校验 String localSign WXPayUtil.generateSignature(notifyMap, wxPayConfig.getKey()); if (!localSign.equals(notifyMap.get(sign))) { return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[签名校验失败]]/return_msg/xml; } // 3. 订单状态校验与更新 String outTradeNo notifyMap.get(out_trade_no); Order order orderService.getByOrderNo(outTradeNo); if (order null || !OrderStatus.WAIT_PAY.name().equals(order.getStatus())) { return xmlreturn_code![CDATA[FAIL]]/return_codereturn_msg![CDATA[订单不存在或状态异常]]/return_msg/xml; } order.setStatus(OrderStatus.PAID.name()); order.setPayTime(new Date()); orderService.updateById(order); return xmlreturn_code![CDATA[SUCCESS]]/return_codereturn_msg![CDATA[OK]]/return_msg/xml; }2.3 前后端分离下的跨域与 Token 处理CorsConfiguration JwtAuthenticationFilterVue3 管理后台运行在http://localhost:8080SpringBoot 后端在http://localhost:8081必须显式配置 CORS。项目未使用CrossOrigin注解粒度太粗而是在WebMvcConfigurer中精细化控制Configuration public class WebConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:8080, https://your-admin-domain.com) // 生产环境填域名 .allowCredentials(true) .maxAge(3600) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS); } }Token 验证则通过自定义过滤器实现避免侵入 Controller 层Component public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { String jwt token.substring(7); try { Claims claims Jwts.parser().setSigningKey(jwtConfig.getSecret()).parseClaimsJws(jwt).getBody(); String username claims.getSubject(); UserDetails userDetails userDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write(Token invalid); return; } } filterChain.doFilter(request, response); } }注意JwtAuthenticationFilter必须注册到 Spring Security 的过滤链中且顺序需在UsernamePasswordAuthenticationFilter之前。实际部署时jwtConfig.getSecret()应从环境变量读取而非硬编码。3. Vue3 管理后台与微信小程序的协同开发要点Vue3 管理后台负责商品上架、订单审核、库存预警等运营动作微信小程序承载用户浏览、加购、下单、支付全流程。二者虽独立部署但共享同一套后端 API因此接口规范、错误码体系、Token 传递方式必须严格对齐。A119 项目在此处做了三项关键约定统一请求拦截器处理 Token、标准化错误响应结构、以及小程序端特有的微信登录态透传。3.1 Vue3 请求拦截器自动注入 Token 与统一错误处理Vue3 使用axios封装请求request.js中定义全局拦截器// src/utils/request.js import axios from axios import { ElMessage } from element-plus const service axios.create({ baseURL: import.meta.env.VUE_APP_BASE_API, // 从 .env 文件读取 timeout: 10000 }) // 请求拦截器自动添加 Authorization 头 service.interceptors.request.use( config { const token localStorage.getItem(admin_token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error) ) // 响应拦截器统一处理业务错误 service.interceptors.response.use( response { const { code, message, data } response.data if (code 200) { return data } else { ElMessage.error(message || 请求失败) return Promise.reject(new Error(message)) } }, error { if (error.response?.status 401) { localStorage.removeItem(admin_token) window.location.href /login } return Promise.reject(error) } ) export default service提示import.meta.env.VUE_APP_BASE_API是 Vite 环境变量约定写法需在.env.development中定义VUE_APP_BASE_APIhttp://localhost:8081/api生产环境对应.env.production。避免将 API 地址硬编码在代码中。3.2 微信小程序端wx.login code 换取 session_key 的完整链路小程序无法直接存储 Cookie所有身份认证依赖wx.login()获取临时登录凭证code再由前端发送给后端调用微信接口换取openid和session_key。A119 小程序端关键代码如下// pages/login/login.js Page({ data: { loading: false }, onGetUserInfo(e) { this.setData({ loading: true }) wx.login({ success: res { // 1. 将 code 发送给后端 wx.request({ url: https://your-api-domain.com/api/wx/login, method: POST, data: { code: res.code }, success: loginRes { const { token, userInfo } loginRes.data // 2. 保存 token 到 storage后续请求自动携带 wx.setStorageSync(token, token) wx.setStorageSync(userInfo, userInfo) wx.switchTab({ url: /pages/index/index }) }, fail: () { wx.showToast({ title: 登录失败, icon: none }) }, complete: () { this.setData({ loading: false }) } }) } }) } })后端接收code后调用微信接口https://api.weixin.qq.com/sns/jscode2session注意必须使用appid、secret和js_code三元组且grant_typeauthorization_code// WxController.java PostMapping(/login) public ResultMapString, Object wxLogin(RequestBody MapString, String params) { String code params.get(code); String url https://api.weixin.qq.com/sns/jscode2session? appid wxConfig.getAppId() secret wxConfig.getSecret() js_code code grant_typeauthorization_code; String result HttpUtil.get(url); // 使用 Hutool 工具类发起 GET 请求 JSONObject json JSON.parseObject(result); if (json.containsKey(openid)) { String openid json.getString(openid); String sessionKey json.getString(session_key); // 生成 JWT Token包含 openid用于后续接口鉴权 String token JwtUtil.generateToken(openid); // 查询或创建用户 User user userService.findByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); user.setCreateTime(new Date()); userService.save(user); } MapString, Object data new HashMap(); data.put(token, token); data.put(userInfo, user); return Result.success(data); } else { return Result.fail(微信登录失败 json.getString(errmsg)); } }3.3 前后端分离项目实战中的调试技巧抓包定位跨域与 Token 问题当小程序页面空白或管理后台报 401 错误时不要先查代码先抓包确认请求是否发出、Header 是否正确、响应内容是否符合预期。推荐组合微信开发者工具打开「调试器」→「Network」筛选XHR查看/api/product/list请求的 Request Headers 是否含Authorization: Bearer xxxResponse Headers 是否有Access-Control-Allow-OriginChrome 浏览器管理后台 F12 → Network → XHR对比请求 URL、Method、Payload、Response StatusPostman手动构造相同请求带AuthorizationHeader验证后端接口是否正常返回 JSON 数据。常见错误模式现象抓包特征根本原因修复方式小程序白屏Network 无请求请求未发出wx.requestURL 协议错误如写成http://而非https://检查request的url字段小程序要求 HTTPS管理后台提示“请求失败”Response 为 HTML返回了 Nginx 默认 404 页面Vue Router history 模式下后端未配置 fallback在 Nginx 配置中添加try_files $uri $uri/ /index.html;登录成功但后续请求 401Request Headers 无 AuthorizationlocalStorage.getItem(admin_token)返回 null检查登录接口是否真返回了 token且前端正确存储4. MySQL 数据库脚本解析与性能优化建议A119 项目附带的sql/a119.sql脚本并非简单建表而是针对生鲜业务特点做了索引预设、字段类型优化和初始数据填充。直接执行即可启动但若要支撑日均万单量需理解其设计逻辑并做针对性调整。4.1 核心表结构与索引策略分析脚本中product商品、order订单、order_item订单项三张表构成交易主干。关键设计点如下表名字段示例类型索引设计意图productid,name,price,stock,category_id,statusBIGINT, VARCHAR, DECIMAL, INT, TINYINT主键id联合索引(category_id, status, id)支持按分类上下架状态查询商品列表覆盖索引避免回表orderorder_no,user_id,total_amount,status,create_time,pay_timeVARCHAR, BIGINT, DECIMAL, TINYINT, DATETIME主键order_no索引(user_id, create_time)索引(status, create_time)支持用户查个人订单按时间倒序、运营查待发货订单按状态时间order_itemorder_no,product_id,quantity,priceVARCHAR, BIGINT, INT, DECIMAL主键(order_no, product_id)索引(product_id)主键满足订单详情查询额外索引支持“某商品卖出多少件”统计注意order_no设计为字符串如ORD20240520123456789而非自增 ID避免暴露订单总量和生成规律同时便于分库分表时做 hash 路由。4.2 生鲜场景下的慢查询优化避免 SELECT * 与深分页脚本中product_list接口默认分页参数为page1size10但若运营人员翻到第 100 页LIMIT 990,10MySQL 仍需扫描前 990 行。A119 项目已在ProductController中采用游标分页替代传统 offset 分页// 使用 last_id 作为游标而非 page/size GetMapping(/list) public ResultPageProduct list(RequestParam Long lastId, RequestParam Integer size) { PageProduct page new Page(1, size); QueryWrapperProduct wrapper new QueryWrapper(); wrapper.gt(id, lastId) // 只查 id lastId 的记录 .orderByAsc(id) .last(LIMIT size); PageProduct result productMapper.selectPage(page, wrapper); return Result.success(result); }对应小程序端下一页请求携带上一页最后一条记录的id// 小程序获取下一页 loadMore() { wx.request({ url: /api/product/list?lastId${this.data.lastId}size10, success: res { const list res.data.records if (list.length 0) { this.setData({ productList: this.data.productList.concat(list), lastId: list[list.length - 1].id // 更新游标 }) } } }) }4.3 SQL 脚本执行与初始化验证 checklist执行a119.sql后务必验证以下五项否则后续功能必然异常验证项执行命令预期结果说明数据库字符集SHOW VARIABLES LIKE character_set_database;utf8mb4确保支持 emoji 和四字节 UTF-8 字符管理员账号存在SELECT * FROM sys_user WHERE username admin;返回 1 条记录password 字段为 BCrypt 加密字符串默认账号密码为admin/123456密码已加密商品分类数据SELECT COUNT(*) FROM category;≥ 5脚本预置了水果、蔬菜、肉禽、水产、熟食五大类微信配置表SELECT * FROM wx_config LIMIT 1;返回 appid、secret、mch_id 等字段若为空需手动 INSERT 或在管理后台配置页填写订单状态枚举SELECT DISTINCT status FROMorder;WAIT_PAY,PAID,DELIVERED,COMPLETED,CANCELLED5 种状态必须齐全否则状态流转逻辑报错5. 微信小程序端加载页定制与首屏性能优化技巧小程序首次打开时的白屏时间直接影响用户留存。A119 项目默认使用微信原生启动页灰色背景加载图标但实际交付中客户常要求替换为品牌 Logo 或动态动画。这并非简单改一张图而是涉及小程序生命周期、分包加载与资源预加载的综合优化。5.1 修改刚进入的加载页面app.json 配置与 splash.png 替换微信小程序的启动页由app.json中的splash字段控制但该字段仅在基础库 2.25.0 支持且需配合project.config.json中的minPlatformVersion设置。A119 项目采用兼容性更强的方案在app.js的onLaunch生命周期中显示自定义 loading待资源加载完毕再隐藏// app.js App({ onLaunch() { // 1. 显示自定义启动页 wx.showLoading({ title: 加载中..., mask: true }) // 2. 预加载关键数据如商品分类、轮播图 Promise.all([ this.loadCategory(), this.loadBanner() ]).then(() { // 3. 隐藏 loading跳转首页 wx.hideLoading() wx.switchTab({ url: /pages/index/index }) }).catch(err { wx.hideLoading() wx.showToast({ title: 启动失败, icon: none }) }) }, loadCategory() { return new Promise((resolve, reject) { wx.request({ url: https://your-api-domain.com/api/category/list, success: res resolve(res.data), fail: reject }) }) } })对应app.json中移除splash配置改为设置window的navigationBarBackgroundColor与backgroundColor保持视觉一致{ window: { navigationBarBackgroundColor: #ffffff, backgroundColor: #f8f8f8, navigationStyle: custom } }提示wx.showLoading的mask: true参数至关重要它阻止用户在启动期间点击任何按钮避免因数据未就绪导致的空列表或报错。5.2 首屏渲染加速分包异步加载与图片懒加载生鲜小程序首页包含轮播图、分类导航、热销商品、新品上架四大区块若全部同步加载首屏 JS 包体积将超 2MB。A119 项目采用分包策略将非首屏组件如“我的”页面、“订单详情”单独打包// app.json { subPackages: [ { root: package-user, pages: [ { path: pages/user/index }, { path: pages/order/detail } ] } ] }同时首页图片使用lazy-load属性启用懒加载!-- pages/index/index.wxml -- swiper autoplay indicator-dots swiper-item wx:for{{bannerList}} wx:keyid image modeaspectFill lazy-load src{{item.imgUrl}}/image /swiper-item /swiper view classcategory-list navigator url/pages/category/index hover-classnone wx:for{{categoryList}} wx:keyid image modeaspectFill lazy-load src{{item.icon}}/image text{{item.name}}/text /navigator /view5.3 微信小程序顶部导航栏高度适配与安全区处理不同机型顶部状态栏高度不同iPhone X 系列有刘海A119 项目在app.wxss中定义通用安全区变量/* app.wxss */ .safe-area-inset-top { padding-top: env(safe-area-inset-top); } .safe-area-inset-bottom { padding-bottom: env(safe-area-inset-bottom); }并在首页 WXML 中应用!-- pages/index/index.wxml -- view classcontainer safe-area-inset-top !-- 顶部导航栏 -- view classnav-bar text classtitle鲜达优选/text /view !-- 内容区域 -- scroll-view classcontent scroll-y !-- 轮播图、分类等 -- /scroll-view /view对应app.json中关闭原生导航栏使用自定义{ window: { navigationStyle: custom } }这样既规避了wx.getSystemInfoSync().statusBarHeight动态计算的兼容性问题又确保内容不被刘海遮挡。实际测试中iPhone 14 Pro 的env(safe-area-inset-top)值为44px而 Android 机型通常为24pxCSS 变量自动适配。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价