news 2026/10/7 10:14:01

微信小程序+SpringBoot+Vue水果店毕设高分实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
微信小程序+SpringBoot+Vue水果店毕设高分实战指南

简介:这是一套面向计算机专业本科生的高分毕业设计级微信小程序水果电商系统,适用于毕设开发、课程设计与期末大作业实战。系统基于Java+SpringBoot构建后端服务,Vue实现管理后台界面,微信小程序提供用户端,MySQL 5.7+支撑核心数据存储,技术栈成熟、文档完整、开箱即用。资源包共1212个文件,涵盖101个Java后端逻辑文件、127个Vue组件、243个JS交互脚本、51个WXML页面结构及57个WXSS样式文件,辅以SQL建库脚本、Navicat数据库配置说明及多套CSS/图标资源,整体38.87MB,结构清晰、模块解耦明确。已有163人学习下载,适合零基础快速部署调试。读者可直接获取完整可运行源码、全量数据库脚本、前后端分离工程结构、小程序项目配置及配套工具链说明,无需二次修改即可完成本地运行与功能验证,显著降低毕设落地门槛。

1. 为什么这个「微信小程序水果店」毕业设计,比你想象中更值得深挖?

不是所有带“SpringBoot+Vue+MySQL”的毕设都配叫高分——真正拉开差距的,从来不是堆砌技术名词,而是在微信小程序这个封闭生态里,把 Java 后端的事务一致性、Vue 前端的路由懒加载与状态管理、MySQL 的库存扣减防超卖、以及微信登录/支付/消息推送这四条线拧成一股绳。我去年帮三个学生重构过类似项目,发现90%的“能跑通”版本,一压测就崩在库存扣减(下单成功但库存没减)、一上线就卡在微信手机号授权回调(前端拿不到 code、后端验签失败)、一打包就报Cannot find module 'vue'(Vue 打包产物没正确嵌入 SpringBoot 静态资源路径)。这不是代码写得丑,而是对微信小程序的运行机制、SpringBoot 的资源加载顺序、Vue CLI 的构建生命周期缺乏穿透式理解。如果你正卡在「部署后白屏」「支付回调收不到」「数据库字段改了但前端列表不更新」这些具体问题上,这篇笔记就是为你写的——它不讲 SpringBoot 是什么,只告诉你:怎么让 Vue 的router.push真正跳转到微信小程序的pages/order/detail页面,怎么用 MySQL 的SELECT ... FOR UPDATE在高并发下锁住一行库存记录,怎么把wx.login()拿到的 code 安全传给 SpringBoot 并完成 session_key 解密。适合正在赶毕设、想拿高分、或刚入职需要快速接手同类项目的 Java/Vue 全栈新人。


2. 后端:SpringBoot 如何扛住微信小程序的高频请求与事务边界

微信小程序的请求模式和传统网页完全不同:单页应用(SPA)但无浏览器缓存控制权、冷启动频繁、用户行为碎片化(刷3秒商品页→点立即购买→切后台→5分钟后回来付款)。这就要求 SpringBoot 不是简单暴露 REST API,而必须在连接复用、事务粒度、异常兜底三处做针对性加固。下面拆解最常翻车的三个模块:登录态管理、库存扣减、支付回调。

2.1 微信登录态:别再用 Session,用 Redis + JWT 做无状态鉴权

微信小程序不支持 Cookie,HttpSession在分布式部署下失效。常见错误是直接把wx.login()返回的code发给后端,后端用appid+secret+code调用微信接口换session_key,然后存在内存 Map 里——这根本没法水平扩展,且session_key有效期仅 2 小时,续期逻辑缺失。

✅ 正确做法:

  • 前端调wx.login()获取code,连同encryptedData和iv一起 POST 到/api/auth/login
  • 后端用WxMaService(com.github.binarywang.wxjava)校验签名并解密手机号(若用户授权),生成JWT Token(含openid,unionid,exp)
  • Token 存 Redis(key:token:${jwt}, value:openid, expire: 7天),同时返回给前端存入wx.setStorageSync('token', token)
// com.example.fruitshop.controller.AuthController.java @PostMapping("/login") public Result<LoginResp> login(@RequestBody LoginReq req) { // 1. 校验 code 有效性(调用微信接口) WxMaJsCode2SessionResult session = wxMaService.jsCode2Session(req.getCode()); String openid = session.getOpenid(); // 2. 若传了 encryptedData,解密手机号(需用户已授权) String phone = null; if (StringUtils.isNotBlank(req.getEncryptedData())) { try { phone = wxMaService.getUserService() .getPhoneNoInfo(session.getSessionKey(), req.getEncryptedData(), req.getIv()) .getPhoneNumber(); } catch (WxErrorException e) { log.warn("手机号解密失败,openid={}", openid, e); } } // 3. 生成 JWT(使用 jjwt-api 0.11.5) String jwt = Jwts.builder() .setSubject(openid) .claim("phone", phone) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, "your-secret-key-here") .compact(); // 4. 写入 Redis(key: token:xxx, value: openid, TTL=7d) redisTemplate.opsForValue().set("token:" + jwt, openid, Duration.ofDays(7)); return Result.success(new LoginResp(jwt, openid, phone)); }

参数说明:your-secret-key-here必须替换成 32 字节随机密钥(可用openssl rand -base64 32生成);Redis key 命名带token:前缀便于批量清理;JWT 过期时间设为 7 天而非微信session_key的 2 小时,因session_key仅用于首次解密,后续鉴权靠 JWT。

2.2 库存扣减:MySQL 行锁 + 乐观锁双保险,防超卖不丢单

微信小程序用户习惯“疯狂点击立即购买”,极易触发并发下单。单纯UPDATE product SET stock = stock - 1 WHERE id = ? AND stock > 0在高并发下会漏锁——因为 WHERE 条件判断和 UPDATE 执行不是原子的。必须用SELECT ... FOR UPDATE显式加行锁,且锁范围要精确到单个商品 ID。

✅ 正确流程(以OrderService.createOrder()为例):

  1. 开启事务(@Transactional)
  2. SELECT * FROM product WHERE id = ? FOR UPDATE—— 锁住该商品行
  3. 检查stock >= orderQuantity,不满足则抛InsufficientStockException
  4. UPDATE product SET stock = stock - ? WHERE id = ?
  5. 插入订单主表 + 订单明细表
// com.example.fruitshop.service.OrderService.java @Transactional(rollbackFor = Exception.class) public Order createOrder(Long productId, Integer quantity, String openid) { // 1. 加行锁(注意:必须走主键索引,否则升级为表锁!) Product product = productMapper.selectByIdForUpdate(productId); // XML 中写 <select ... for update> if (product == null || product.getStock() < quantity) { throw new InsufficientStockException("库存不足,商品ID:" + productId); } // 2. 扣减库存(此时行锁仍在) int updated = productMapper.decreaseStock(productId, quantity); if (updated != 1) { throw new RuntimeException("库存扣减失败"); } // 3. 创建订单(关联 openid) Order order = new Order(); order.setProductId(productId); order.setQuantity(quantity); order.setOpenid(openid); order.setStatus("UNPAID"); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); return order; }

关键细节:selectByIdForUpdate对应的 XML 必须显式写FOR UPDATE(MyBatis 不自动加);decreaseStock的 SQL 是UPDATE product SET stock = stock - #{quantity} WHERE id = #{id};绝对不要在 SELECT 后 sleep() 或做耗时操作,否则锁持有时间过长,拖垮吞吐量。

2.3 支付回调:用幂等性 Key + 状态机,杜绝重复发货

微信支付回调地址(notify_url)可能被重复推送(网络抖动、超时重试),若不做幂等处理,会导致同一笔订单创建多个发货单。常见错误是只校验out_trade_no是否存在,但未考虑“创建订单成功→支付回调→发货→又来一次回调”的场景。

✅ 正确方案:

  • 回调接收接口/api/pay/notify入口加synchronized锁(按out_trade_no分桶)
  • 解析微信回调 XML,校验签名(WXPayUtil.verifySignature(xmlStr, mchKey))
  • 查询订单状态,仅当订单为UNPAID时才更新为PAID并触发发货
  • 更新后插入幂等日志表pay_notify_log(字段:out_trade_no,notify_id,status,create_time)
// com.example.fruitshop.controller.PayController.java @PostMapping(value = "/notify", produces = MediaType.TEXT_PLAIN_VALUE) public String handlePayNotify(HttpServletRequest request) { try { // 1. 读取回调XML String xmlStr = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); // 2. 校验签名(必须!防止伪造回调) Map<String, String> notifyMap = WXPayUtil.xmlToMap(xmlStr); if (!WXPayUtil.isSignatureValid(notifyMap, "your-mch-key-here")) { return "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[签名失败]]></return_msg></xml>"; } // 3. 幂等校验(按 out_trade_no 加锁) String outTradeNo = notifyMap.get("out_trade_no"); synchronized (outTradeNo.intern()) { // 字符串常量池锁,避免新建对象 // 查询订单 Order order = orderMapper.selectByOutTradeNo(outTradeNo); if (order == null || !"UNPAID".equals(order.getStatus())) { return "<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>"; } // 4. 更新订单状态 + 发货(此处可发 MQ 异步处理) order.setStatus("PAID"); order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order); // 5. 记录幂等日志 PayNotifyLog log = new PayNotifyLog(); log.setOutTradeNo(outTradeNo); log.setNotifyId(notifyMap.get("result_code")); // 微信回调唯一ID log.setStatus("PROCESSED"); payNotifyLogMapper.insert(log); } 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>"; } }

参数说明:your-mch-key-here是微信商户平台的 API 密钥(32位);synchronized (outTradeNo.intern())利用字符串常量池实现轻量级分桶锁,比 Redis 分布式锁更简单;幂等日志表必须建唯一索引UNIQUE KEY uk_out_trade_no (out_trade_no)。


3. 前端:Vue 如何适配微信小程序的运行约束与性能瓶颈

Vue CLI 打包出的 SPA 无法直接在微信小程序环境运行——小程序没有window、document、localStorage,且要求所有页面路径在app.json中静态声明。因此,真正的“Vue + 微信小程序”不是把 Vue 项目扔进去,而是用 Vue 语法写小程序逻辑,再通过uni-app或mpvue编译成小程序原生代码。本项目采用uni-app(国内生态最成熟),它允许你用 Vue 语法开发,一键编译到微信小程序、H5、App 多端。

3.1 项目结构:uni-app 的标准目录与微信特有配置

uni-app项目根目录下必须有pages.json(替代小程序app.json)、manifest.json(配置 AppID)、uni.scss(全局样式变量)。关键区别在于:

  • pages目录下的.vue文件会被编译成小程序的pages/xxx/xxx.wxml
  • static目录资源(图片、字体)直接映射到小程序static/
  • components下的.vue组件需用<import>引入,不能用import
// pages.json(核心!决定小程序导航栏、页面路径) { "pages": [ { "path": "pages/index/index", "style": { "navigationBarTitleText": "鲜果直送", "enablePullDownRefresh": true, "onReachBottomDistance": 50 } }, { "path": "pages/product/detail", "style": { "navigationBarTitleText": "商品详情", "usingComponents": true } } ], "subNVue": [], "tabBar": { "color": "#7A7E83", "selectedColor": "#3cc51f", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/index", "iconPath": "static/tabbar/home.png", "selectedIconPath": "static/tabbar/home-active.png", "text": "首页" } ] } }

注意:tabBar的pagePath必须和pages数组中的path完全一致;iconPath图片必须放在static目录下,且尺寸为 81px×81px;usingComponents: true表示启用自定义组件(如购物车悬浮按钮)。

3.2 微信能力调用:登录、获取手机号、支付,三步闭环

微信小程序的 API 调用必须在真机调试,模拟器无效。uni-app封装了uni.login、uni.getUserInfo(已废弃)、uni.getPhoneNumber(需用户主动授权)等 API,但必须配合后端完成完整链路。

✅ 完整流程(以“一键授权手机号”为例):

  1. 前端调uni.getPhoneNumber({ withCredentials: true })→ 用户点击授权按钮
  2. 微信返回code(非wx.login()的 code!)和encryptedData
  3. 前端将code和encryptedData发给后端/api/auth/bind-phone接口
  4. 后端用WxMaService.getUserService().getPhoneNoInfo()解密
// pages/user/bind-phone.vue export default { methods: { async bindPhone() { try { // 1. 调用微信获取手机号(需 button 绑定 open-type="getPhoneNumber") const res = await uni.getPhoneNumber({ withCredentials: true // 必须为 true,否则 encryptedData 为空 }) const { code, encryptedData, iv } = res.detail // 2. 发送给后端绑定 const { data } = await this.$http.post('/api/auth/bind-phone', { code, encryptedData, iv }) uni.showToast({ title: '绑定成功', icon: 'success' }) this.$store.commit('SET_PHONE', data.phone) // Vuex 存储 } catch (err) { console.error('获取手机号失败', err) uni.showToast({ title: '授权失败,请重试', icon: 'none' }) } } } }

血泪经验:withCredentials: true是关键,漏写则encryptedData为空字符串;uni.getPhoneNumber必须由<button open-type="getPhoneNumber">触发,不能在onLoad自动调用,否则微信拦截;解密失败常见原因是session_key过期(需重新wx.login()换新 key)。

3.3 性能优化:懒加载、分包、骨架屏,让首屏快于 1 秒

微信小程序包大小限制 2MB(主包),超过需分包。uni-app默认不开启分包,导致pages/product/list.vue和pages/order/detail.vue全部打进主包,首屏加载慢。

✅ 三步优化:

  1. 路由懒加载:pages.json中pages数组的path改为异步导入
  2. 分包配置:在pages.json的subPackages字段声明分包路径
  3. 骨架屏:pages/index/index.vue用v-if/v-else切换 loading 状态
// pages.json(分包配置) { "subPackages": [ { "root": "subPackages/product", "pages": [ { "path": "list/list", "style": { "navigationBarTitleText": "商品列表" } } ] } ] }
<!-- pages/index/index.vue --> <template> <view class="index"> <!-- 骨架屏 --> <view v-if="!loaded" class="skeleton"> <view class="skeleton-item" v-for="i in 3" :key="i"></view> </view> <!-- 真实内容 --> <view v-else> <swiper :indicator-dots="true" :autoplay="true"> <swiper-item v-for="item in banners" :key="item.id"> <image :src="item.url" mode="aspectFill" /> </swiper-item> </swiper> <view class="goods-list"> <navigator v-for="g in goods" :key="g.id" :url="'/subPackages/product/list/list?category=' + g.category" > <image :src="g.cover" /> <text>{{ g.name }}</text> </navigator> </view> </view> </view> </template> <script> export default { data() { return { loaded: false, banners: [], goods: [] } }, onLoad() { // 1. 先展示骨架屏 this.loaded = false // 2. 请求数据(uni.request 自动带上 cookie?不!需手动加 token) uni.request({ url: 'https://api.fruitshop.com/api/product/banner', header: { 'Authorization': 'Bearer ' + uni.getStorageSync('token') }, success: (res) => { this.banners = res.data this.goods = res.data.goods this.loaded = true // 数据加载完才显示真实内容 } }) } } </script>

提示:subPackages下的页面 URL 必须带前缀/subPackages/xxx/;uni.request的header必须手动传Authorization,因为小程序不自动携带 Cookie;骨架屏 CSS 用background: linear-gradient(...)模拟灰色块,比v-show更节省渲染。


4. 数据库:MySQL 5.7 如何设计才能支撑水果店的实时库存与订单查询

水果店业务看似简单,但数据库设计稍有不慎就会成为性能瓶颈:凌晨补货时批量更新库存、大促时订单表每秒写入上百条、用户搜索“苹果”要秒出结果。本项目采用 MySQL 5.7(兼容性最好,云服务器默认版本),不追求最新特性,而聚焦索引、分区、字符集三大落地细节。

4.1 表结构设计:用BIGINT主键、TINYINT状态、JSON扩展字段

微信小程序用户量不大(毕设场景),但表结构必须预留扩展性。常见错误是用VARCHAR(255)存商品描述(浪费空间)、用INT存订单状态(未来加状态要改表)、用TEXT存商品规格(无法建索引)。

✅ 推荐结构(精简版):

表名字段类型说明
productidBIGINT UNSIGNED PK避免负数,支持 2^64 行
nameVARCHAR(100)商品名,加索引
stockINT DEFAULT 0库存,禁止 NULL
priceDECIMAL(10,2)精确到分,不用 FLOAT
specsJSON规格如{"weight":"500g","origin":"山东"},MySQL 5.7+ 支持
orderidBIGINT UNSIGNED PK同上
out_trade_noVARCHAR(64) UNIQUE微信支付单号,建唯一索引
openidVARCHAR(64)用户标识,建索引
statusTINYINT DEFAULT 00=UNPAID, 1=PAID, 2=SHIPPED...,查状态快
create_timeDATETIME建联合索引(status, create_time)
-- 创建 product 表(utf8mb4 支持 emoji,如 🍎) CREATE TABLE `product` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '商品名称', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `price` decimal(10,2) NOT NULL COMMENT '价格', `specs` json DEFAULT NULL COMMENT '规格 JSON', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_name` (`name`) USING BTREE, KEY `idx_stock` (`stock`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表'; -- 创建 order 表(重点:status + create_time 联合索引) CREATE TABLE `order` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `out_trade_no` varchar(64) NOT NULL UNIQUE COMMENT '微信支付单号', `openid` varchar(64) NOT NULL COMMENT '用户openid', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '订单状态:0-待支付,1-已支付,2-已发货', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status_ctime` (`status`,`create_time`) USING BTREE, KEY `idx_openid` (`openid`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

参数说明:utf8mb4是必须的,否则微信昵称里的 emoji(如 👩‍🌾)会乱码;TINYINT比VARCHAR(20)查状态快 10 倍;idx_status_ctime索引让“查今天待发货订单”(WHERE status=1 AND create_time > '2024-06-01')走索引扫描,而非全表扫描。

4.2 索引优化:为什么WHERE name LIKE '%苹果%'永远走不了索引?

微信小程序搜索框输入“苹果”,后端执行SELECT * FROM product WHERE name LIKE '%苹果%',这是典型的索引失效场景——%在左边,B+树索引无法定位。

✅ 正确方案:

  • 方案1(推荐):用LIKE '苹果%'(前缀匹配),要求用户输入后自动补全,如输入“苹”→提示“苹果、苹果汁、苹果醋”
  • 方案2:建全文索引(FULLTEXT(name)),但 MySQL 5.7 对中文支持差,需配合ngram插件
  • 方案3(毕设够用):加CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,让LIKE对中文更友好
-- 为 name 字段加前缀索引(节省空间) ALTER TABLE product ADD KEY `idx_name_prefix` (`name`(20)); -- 查询时强制用前缀(用户输入“苹果”,后端拼成 "苹果%") SELECT * FROM product WHERE name LIKE '苹果%';

避坑:name字段长度 100,但索引只取前 20 字符(name(20)),足够覆盖大部分商品名;COLLATE utf8mb4_unicode_ci让ORDER BY name按中文拼音排序,而非乱码。

4.3 数据安全:备份脚本 + 敏感字段加密,毕业答辩不露怯

毕设演示时,数据库里若出现admin账号密码明文、用户手机号裸奔,答辩老师一眼看出安全漏洞。MySQL 本身不提供字段级加密,需在应用层处理。

✅ 实施步骤:

  1. 密码字段:用BCryptPasswordEncoder(Spring Security)加密,永远不存明文
  2. 手机号字段:用 AES-128-CBC 加密(密钥存application.yml,绝不硬编码)
  3. 每日备份:Linux 下写 crontab,用mysqldump导出 SQL
# application.yml(密钥单独配置) fruitshop: crypto: phone-key: "a1b2c3d4e5f67890" # 16字节 AES 密钥 phone-iv: "0123456789abcdef" # 16字节 IV
// 工具类 PhoneCrypto.java @Component public class PhoneCrypto { private final String key; private final String iv; public PhoneCrypto(@Value("${fruitshop.crypto.phone-key}") String key, @Value("${fruitshop.crypto.phone-iv}") String iv) { this.key = key; this.iv = iv; } public String encrypt(String phone) { try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec); byte[] encrypted = cipher.doFinal(phone.getBytes(StandardCharsets.UTF_8)); return Base64.getEncoder().encodeToString(encrypted); } catch (Exception e) { throw new RuntimeException("手机号加密失败", e); } } public String decrypt(String encryptedPhone) { try { Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(), "AES"); IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes()); cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); byte[] decoded = Base64.getDecoder().decode(encryptedPhone); byte[] decrypted = cipher.doFinal(decoded); return new String(decrypted, StandardCharsets.UTF_8); } catch (Exception e) { throw new RuntimeException("手机号解密失败", e); } } }

注意:key和iv必须 16 字节,可用openssl rand -hex 16生成;加密后存入数据库的phone_encrypted字段(VARCHAR(255));解密在UserMapper.selectById()后手动调用,不在 SQL 层解密(避免慢查询)。


5. 避坑指南:95% 的毕设翻车都发生在这 5 个地方

别等答辩前夜才发现问题。以下是我帮学生 debug 时记录的真实踩坑案例,按现象→原因→解决三步写清,每个都能省你 3 小时。

5.1 现象:微信小程序真机调试白屏,控制台报Cannot find module 'vue'

原因:uni-app项目npm run build:mp-weixin打包后,dist/build/mp-weixin目录被直接拖进微信开发者工具,但node_modules未复制,且project.config.json中miniprogramRoot指向错误路径。
解决:

  • 确保project.config.json的miniprogramRoot为"./dist/build/mp-weixin/"(末尾斜杠不能少)
  • 删除dist/build/mp-weixin/node_modules(uni-app 打包已内置依赖,此目录是冗余的)
  • 微信开发者工具 → 详情 → 本地设置 → 取消勾选“使用 npm 模块”(uni-app 不走 npm)

5.2 现象:SpringBoot 启动报错Caused by: java.lang.ClassNotFoundException: javax.xml.bind.DatatypeConverter

原因:JDK 11+ 移除了 JAXB(Java EE 模块),而wxjava依赖javax.xml.bind。
解决:

  • 在pom.xml中添加 JAXB 依赖:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency> <dependency> <groupId>runtime</groupId> <artifactId>com.sun.xml.bind</artifactId> <version>2.3.1</version> </dependency>

5.3 现象:MySQL 插入中文乱码,日志显示????

原因:MySQL 服务端、数据库、表、连接四层字符集不统一,常见是my.cnf中character-set-server=utf8(错误!应为utf8mb4)。
解决:

  • 修改/etc/my.cnf:
[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' skip-character-set-client-handshake = 1
  • 重启 MySQL:sudo systemctl restart mysqld
  • 重建数据库:CREATE DATABASE fruitshop CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

5.4 现象:Vue 页面跳转后,微信小程序顶部导航栏高度异常(文字被遮挡)

原因:pages.json中navigationStyle: custom时,未在页面.vue中手动设置padding-top: var(--status-bar-height)。
解决:

  • 在pages/index/index.vue的<style>中加:
.content { padding-top: var(--status-bar-height); /* 其他样式 */ }
  • 确保pages.json的style中navigationStyle为"custom"且navigationBarTextStyle设为"black"(否则状态栏文字看不见)

5.5 现象:支付回调成功,但订单状态没变,pay_notify_log表无记录

原因:微信回调地址notify_url配置为http://localhost:8080/api/pay/notify(内网地址),微信服务器无法访问。
解决:

  • 使用natapp.cn或cpolar.com映射内网端口(免费隧道)
  • 微信商户平台 → 产品中心 → 开发配置 →notify_url填https://xxx.ngrok.io/api/pay/notify
  • 务必关闭 SpringBoot 的 HTTPS 重定向(server.http.port=8080,server.ssl.enabled=false),否则隧道转发失败

6. 高分技巧:用 Docker 一键部署 + 自动化测试,让答辩老师眼前一亮

毕设答辩时,老师最想看到的不是“我写了 5000 行代码”,而是“我解决了真实工程问题”。部署和测试环节最容易拉开差距——别人还在手动导 SQL、改配置,你已经docker-compose up -d一键拉起全套环境;别人靠截图证明功能,你用 Jest + Cypress 跑通 20 个核心用例。这才是高分的关键。

6.1 Docker 化部署:3 个文件搞定 MySQL + SpringBoot + Nginx

微信小程序后端必须有公网域名(微信支付要求),本地 IP 不行。Docker 能屏蔽环境差异,让老师在自己电脑上git clone && docker-compose up即可运行。核心是docker-compose.yml、Dockerfile、.env三文件。

# docker-compose.yml version: '3.8' services: db: image: mysql:5.7 container_name: fruitshop-mysql environment: MYSQL_ROOT_PASSWORD: root123 MYSQL_DATABASE: fruitshop MYSQL_USER: app MYSQL_PASSWORD: app123 ports: - "3306:3306" volumes: - ./mysql-data:/var/lib/mysql - ./init.sql:/docker-entrypoint-initdb.d/init.sql backend: build: ./backend container_name: fruitshop-backend ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=docker - SPRING_DATASOURCE_URL=jdbc:mysql://db:3306/fruitshop?useSSL=false&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_USERNAME=app - SPRING_DATASOURCE_PASSWORD=app123 depends_on: - db nginx: image: nginx:alpine container_name: fruitshop-nginx ports: - "80:80" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./dist/build/mp-weixin:/usr/share/nginx/html
# backend/Dockerfile FROM openjdk:11-jre-slim VOLUME /tmp ARG JAR_FILE=target/fruitshop-0.0.1-SNAPSHOT.jar COPY ${JAR_FILE} app.jar ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]
# .env(敏感信息外置) MYSQL_ROOT_PASSWORD=root123 SPRING_DATASOURCE_PASSWORD=app123

验证方法:docker-compose up -d后,访问 `http://localhost/swagger

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/7 10:13:46

零代码RAG智能客服实战:LangFlow流量包推荐与对话记忆

简介&#xff1a;这份资源面向希望快速上手大模型应用开发的技术人员与AI爱好者&#xff0c;基于LangFlow零代码框架&#xff0c;演示如何搭建流量包推荐智能客服&#xff0c;并融合RAG检索增强生成与对话记忆能力&#xff0c;同时兼容GPT系列与国产大模型&#xff0c;提供两种…

作者头像 李华
网站建设 2026/10/7 10:13:35

springboot图书馆管理系统:从前后端分离到部署避坑完整指南

简介&#xff1a;一份基于Spring Boot的图书馆管理系统完整源码包&#xff0c;采用前后端分离架构并附带毕业设计论文&#xff0c;适合高校学生和Java开发者用于毕业设计、课程设计或二次开发。系统功能包括图书增删改查、用户管理、借阅管理、逾期处理、图书检索及借阅历史统计…

作者头像 李华
网站建设 2026/10/7 10:11:47

校园网自动重连工具:断网自动检测 + Portal 自动登录,全自动守护

—— 一个 Go 编写的 Windows 小工具&#xff0c;解决"校园网老断网、总要手动重连"的烦恼一、痛点与解决思路在校园里&#xff0c;WiFi 断连几乎是每天都会遇到的事&#xff1a;信号波动、后台重启、认证过期……一旦断网&#xff0c;就得手动重新连接 WiFi&#xf…

作者头像 李华
网站建设 2026/10/7 10:11:32

DeepLens 理解优先复现路线 · 执行笔记

DeepLens 理解优先复现路线 执行笔记 承接上篇&#xff1a;《DeepLens 论文精读 复现笔记》&#xff08;完整复现口径&#xff0c;P0–P4 L1–L3 成本账&#xff09; 定位转变&#xff1a;目的是深入理解论文&#xff0c;不是复刻论文数字——预训练类环节一律跳过 关键资产…

作者头像 李华