作为一个前后端都写过不少东西的开发者,我去年帮师弟完成了一个基于SpringBoot的校园订餐微信小程序项目,从技术选型、数据库设计到最终部署,整个过程踩了不少坑,也沉淀了一些可复用的经验。今天就把这套完整的设计思路、核心实现和部署要点整理出来,希望能给正在做类似课设、毕设,或者想入门微信小程序开发的朋友一些参考。
这个项目本身不算复杂,但胜在功能链路完整,覆盖了一个典型业务系统从用户端到管理端的所有关键环节——小程序端负责用户点餐、购物车、订单管理,后端SpringBoot应用处理业务逻辑、权限校验和微信登录,数据库则存储用户、菜品、订单、评价等核心数据。换句话说,它足够让你摸清一个真实商业项目的基本骨架。
如果你正打算从零开始做一个「SpringBoot + 微信小程序」的全栈项目,或者接手一套现成的源码一知半解,这篇内容就是为你准备的。
1. 项目整体设计与技术选型
1.1 为什么是SpringBoot + 微信小程序
先说后端。SpringBoot在Java生态里几乎是事实标准,它最大的价值是让很多人头疼的Spring配置变得极简。以前搭一个SSM项目,光XML配置文件就要写半天,引入依赖还得自己处理版本冲突;SpringBoot则通过自动配置把这些重复劳动干掉了。你在项目里写上spring-boot-starter-web、mybatis-plus-boot-starter,几行代码就能把Web服务跑起来,这对开发效率和维护来说价值巨大。
再说前端。校园订餐这件事,用户核心诉求是“打开就能用、用完就关”,微信小程序完美命中了这个场景。它不需要下载安装,扫码或搜索即可打开,微信生态内还自带支付、登录、订阅消息等基础设施。跟开发原生App相比,小程序省去了应用市场审核、版本更新的麻烦;跟做H5网页相比,小程序的API能力更接近原生体验,比如获取用户定位、调用微信支付、订阅消息推送。
这就是我推荐这套组合的根本原因:后端用SpringBoot快速搭服务,前端用微信小程序覆盖用户高频使用场景,二者通过RESTful API通信,分工清晰,适合开发周期有限的课程设计和毕业设计。
1.2 系统功能模块怎么划分
这套系统我建议分成两个核心端:用户端(小程序)和管理端(小程序内嵌或后台网页)。用户端面向学生群体,管理端面向食堂商家或系统管理员。所有功能模块围绕“点餐—支付—履约—评价”这条完整链路展开。
用户端核心功能包括:
- 微信登录:用户打开小程序,通过
wx.login获取临时code,后端调用微信接口换取openid,实现免注册登录。 - 浏览菜品:按分类展示菜品列表,支持搜索、查看菜品详情(图片、描述、价格、评分),菜品还可以打上“招牌推荐”之类的标签。
- 购物车:用户将菜品加入购物车,支持修改数量、删除、清空、结算。
- 提交订单:从购物车生成订单,选择配送地址(校园宿舍楼栋+门牌号)、备注口味偏好,提交后调用微信支付。
- 订单管理:查看当前和历史订单状态,包括待付款、待接单、配送中、已完成、已取消;订单完成后可进行评价和追评。
- 个人中心:管理收货地址、查看评价记录、联系客服等。
管理端(这里我用了一个独立的管理后台页面包在管理员的视角里)核心功能包括:
- 商家入驻/角色配置:管理员审核商家账号,分配对应食堂窗口。
- 菜品管理:上下架菜品、维护菜品种类和库存、设置每日特价。
- 订单管理:查看所有用户订单,接单、标记配送、完成订单等。
- 数据统计:展示今日订单量、营业额、热门菜品排行等基础运营数据。
一开始我差点把管理端做成一个独立的小程序,后来发现这样做有两个问题:一是小程序审核时如果涉及商家入驻、虚拟支付等内容,审核周期会拉长;二是管理端通常不需要高频移动操作,用后台管理系统管理起来效率更高,界面也更容易做出复杂的数据分析图表。所以最终我给管理端单独设计了一套基于Thymeleaf或者Vue的后台界面,这里就不展开讲了。
1.3 技术栈清单
整个项目我用到的核心技术栈如下:
| 层级 | 技术选型 | 说明 |
|---|---|---|
| 后端语言 | Java 8+(建议Java 11或17) | SpringBoot对这两个版本兼容极佳 |
| 后端框架 | SpringBoot 2.7.x 或 3.x | 2.7.x更稳,3.x要求JDK17+,按自己环境选 |
| ORM | MyBatis Plus | 单表CRUD不用写SQL,分页也封装的很好 |
| 数据库 | MySQL 5.7+ 或 8.0 | 8.0对JSON类型、性能优化更友好 |
| 缓存/分布式会话 | Redis(可选) | 用于缓存验证码、维持登录态、限流 |
| 鉴权方案 | JWT 或 Sa-Token | 无状态,适合前后端分离架构 |
| 前端框架 | 微信小程序原生 + Vant Weapp组件库 | 用原生+UI库,减少学习成本和兼容问题 |
| 接口文档 | Swagger / knife4j | 调试接口,写答辩文档时特别好用 |
| 部署 | Nginx + Docker(可选)或直接jar包 | 服务器资源紧张时直接java -jar即可 |
| 工具链 | Maven、Git、宝塔面板 | 简化部署,提升开发协作效率 |
这里需要特别说一个坑:曾经有同学图新直接用SpringBoot 3.x,结果很多老版本的MyBatis Plus和Spring Security还需要额外适配,反向踩了不少坑。如果是做课设或毕设,时间紧张的话建议直接使用SpringBoot 2.7.x,不用太纠结。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与后端核心实现
2.1 核心表结构与关系设计
数据库设计是整个项目的根基,表结构设计好了,后面的业务代码就是水磨工夫。这套系统我最终设计了七张核心表,它们之间的联系可以用几句话梳理清楚:
- 用户表(user):存储用户openid、昵称、头像、手机号、默认地址ID等。用户不需要注册,第一次微信登录成功后自动创建。
- 分类表(category):存储菜品分类名称和排序值,比如“招牌套餐”“饮品甜点”“米饭简餐”。
- 菜品表(dish):存储菜品名称、图片、描述、原价、现价、库存、是否推荐、所属分类。字段尽量拆细,像“辣度”“标签”这类可以用JSON存。
- 购物车表(cart):以用户ID为维度记录购物车数据,菜品ID、数量、勾选状态、加购时间。这里要注意用
user_id + dish_id做唯一索引,防止同一菜品被反复插入生成多条脏数据。 - 订单表(orders):存储订单编号、用户ID、总金额、配送地址、备注、订单状态、支付时间、接单时间、完成时间。订单编号建议用时间戳+随机数,避免直接暴露自增ID导致刷单风险。
- 订单明细表(order_detail):存储每个订单涉及的菜品快照(菜品名称、图片、单价、数量),防止后续菜品下架导致历史订单无法追溯。
- 评价表(comment):关联订单和菜品,用户可以打星、填写文字评论、上传图片。
在具体建表时,我有几个切身体会:
- 金额一律用decimal(10,2),绝不用float,否则容易出现金额精度问题,比如订单总额显示成49.999999。
- 订单状态用int或tinyint,例如0待付款、1待接单、2配送中、3已完成、4已取消、5退款中,在代码里用常量或枚举去匹配,不要直接在业务里写死数字。
- 统一添加create_time、update_time、is_deleted字段,虽然有时候感觉冗余,但在统计和排查问题时非常有用。
2.2 小程序登录与鉴权方案
小程序端使用的登录流程是微信生态基本操作,但也是很多新手最容易搞不清楚的地方。
大致流程如下:
- 小程序端调用
wx.login(),获取一个临时凭证code,这个code有效期只有5分钟,只能使用一次。 - 小程序把这个code通过后端接口传给SpringBoot应用。
- 后端拿着这个code,配合小程序的AppID、AppSecret,向微信接口
https://api.weixin.qq.com/sns/jscode2session发送请求,换取openid和session_key。 - openid是用户在某个小程序下的唯一标识,同一个用户在不同的小程序下openid不同;后端查出或创建用户记录,然后生成一个自定义登录态(JWT token)返回给小程序。
- 小程序后续每次请求接口时,在请求头携带这个token,后端通过拦截器校验token的有效性并识别当前用户身份。
我用的JWT结构大概是这样的:
java复制String token = Jwts.builder()
.setSubject(String.valueOf(user.getId()))
.setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 3600 * 1000L))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
然后写一个SpringMVC拦截器,在preHandle里统一检查请求头里的token,解析出用户ID再放入ThreadLocal,这样后续的Service层就不用每个方法都传一遍userId了。
这里我踩过最多的坑就是:很多新手直接把AppSecret写在小程序前端代码里。这是极度危险的,AppSecret一旦泄露,任何人都可以伪造登录信息。正确的做法是AppSecret只存放在后端配置里,小程序端永远只传code。
2.3 订单状态机与超时处理
校园订餐业务的订单状态切换其实就是一个典型的“状态机”问题。我起初用if-else嵌套写订单状态变更,后来发现代码越写越乱,测试时也总是漏掉状态组合。后来我改用枚举加状态流转表的方式,把每一种合法流转都定义清楚。
订单状态切换逻辑如下:
- 待付款(0):提交订单后创建,用户可在15分钟内支付,超时自动取消。
- 待接单(1):支付成功后进入商家待接单列表,商家可接单或拒单。
- 配送中(2):商家接单后制餐并配送,学生可见骑手(或楼下取餐)状态。
- 已完成(3):用户确认收货或系统自动确认,订单终态,用户可评价。
- 已取消(4):用户主动取消、超时未支付、商家拒单都会进入此状态。
对于超时未支付的订单,我之前用过定时任务扫描,后来发现更轻量的方案是使用延时队列或Redis的过期键通知机制,但考虑到项目规模,简易的做法是:启动一个定时任务,每30秒扫描一次待付款且支付超时的订单,将其状态改为已取消并恢复库存。如果追求性能再考虑延时消息中间件,但对校园订餐这个量级来说定时任务完全够用。
2.4 购物车与菜品库存的并发控制
这里聊一个很容易被忽视的细节。用户下单扣减库存时,如果直接用代码:
java复制if (dish.getStock() < quantity) {
throw new BusinessException("库存不足");
}
dish.setStock(dish.getStock() - quantity);
updateById(dish);
在高并发场景下,多个请求同时读到同样的库存数量,会出现“超卖”问题。虽然校园订餐的并发量可能没有那么夸张,但到了所谓“午餐高峰”,某个热门食堂窗口的抢购频率还是挺高的。所以我在扣库存时用了乐观锁:
java复制boolean success = dishMapper.deduceStock(dishId, quantity) > 0;
对应的SQL类似:
sql复制UPDATE dish SET stock = stock - #{quantity}
WHERE id = #{dishId} AND stock >= #{quantity}
这样利用数据库的原子操作,从根本杜绝了超卖风险。同时购物车和订单的创建应该放在同一个事务里,一旦任何一个步骤失败,整体回滚,保证数据一致性。
3. 小程序端实现与前后端联调
3.1 小程序页面结构和导航设计
小程序的目录结构一般是这样:
text复制├── pages/
│ ├── index/ // 首页:菜品分类和列表
│ ├── cart/ // 购物车
│ ├── order/ // 订单确认页
│ ├── orderList/ // 订单列表
│ ├── mine/ // 个人中心
├── components/ // 自定义组件:菜品卡片、数量选择器
├── utils/
│ ├── request.js // 封装wx.request
│ ├── auth.js // 登录态处理
├── app.js
├── app.json
底部TabBar我设置的四个入口:首页、购物车、订单、我的。在app.json里配置:
json复制{
"tabBar": {
"color": "#999999",
"selectedColor": "#ff6600",
"list": [
{ "pagePath": "pages/index/index", "text": "首页", "iconPath": "", "selectedIconPath": "" },
{ "pagePath": "pages/cart/cart", "text": "购物车", "iconPath": "", "selectedIconPath": "" },
{ "pagePath": "pages/orderList/orderList", "text": "订单", "iconPath": "", "selectedIconPath": "" },
{ "pagePath": "pages/mine/mine", "text": "我的", "iconPath": "", "selectedIconPath": "" }
]
}
}
关于小程序顶部导航栏的适配,是我遇到的一个小麻烦。不同型号手机的胶囊按钮位置、状态栏高度都不一样,如果自定义导航栏很容易出现错位。经验是:尽量先用微信原生导航栏,等后期有美化需要再去自定义;如果非自定义不可,最好通过wx.getWindowInfo()去动态获取状态栏高度,然后给自定义导航栏设置相应的padding-top值。
3.2 请求封装与统一登录态管理
小程序发接口请求不能像浏览器那样随意跨域,所以我把wx.request统一封装到一个request.js里,增加baseURL、超时时间、请求头注入,以及401状态码的自动重新登录逻辑。
一个简化版的封装思路是这样的:
javascript复制const request = async (url, method = 'GET', data = {}) => {
const token = wx.getStorageSync('token');
return new Promise((resolve, reject) => {
wx.request({
url: baseUrl + url,
method,
data,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : ''
},
timeout: 10000,
success: (res) => {
if (res.statusCode === 200) {
if (res.data.code === 401) {
// token失效,重新走登录流程
reloginAndRetry(url, method, data, resolve, reject);
} else {
resolve(res.data);
}
} else {
reject(res);
}
},
fail: (err) => {
wx.showToast({ title: '网络异常', icon: 'none' });
reject(err);
}
});
});
};
在登录环节,我会在小程序启动时先读取本地缓存的token,如果token存在并且没有过期就直接使用;如果不存在,再走wx.login换code的流程。这样能避免用户每次打开小程序都重复静默登录,节省网络开销,也能减少因微信接口偶发不稳定导致的“登录失败”问题。
3.3 菜品列表、购物车和订单确认页的实现细节
首页菜品列表,我采用左右分栏布局,左侧是分类,右侧是菜品列表。这个布局在小程序里实现很简单:左侧一个scroll-view,右侧一个scroll-view,各自绑定不同的滚动事件。需要注意的是,右侧列表如果用scroll-view,里面嵌套van-card这类组件时,很多组件自带的点击事件会因滚动手势冲突而不生效,所以我后来改用页面级滚动,配合position: sticky实现左侧分类吸顶。
购物车页面,我直接绑定一个本地购物车状态,在加购、减购时同步更新这个小车,等到结算时一次性提交后端。为什么不每次加购都请求后端保存购物车?原因很简单:用户在选菜过程中会频繁翻看、调整数量,如果每次都走后端接口,体验会卡顿,也会给服务器制造很多无谓压力。因此购物车数据我做成了“本地优先、提交时才同步”模式,只有当用户真正点击结算时才把购物车数据拉到后端生成订单。
订单确认页,核心是从购物车中读取选中的菜品生成订单预览,让用户确认收货地址、备注、金额,然后点击“提交订单”。地址选择这里其实也有坑——如果直接在订单确认页要求用户填写收货地址,体验会比较重。我做成点击地址栏时弹出一个半屏页面,里面展示已有的地址列表,也可以新增地址,然后选中后回填。小程序里用wx.navigateBack带参或者通过全局事件总线传值都可以。
3.4 支付流程(微信支付)
说到微信支付,我不得不提醒一句:个人主体的小程序无法开通微信支付,只有企业主体或个体工商户才行。如果你做的是毕设或课设,通常没有企业资质,这里有两种处理方式:
- 接入微信支付沙箱/模拟支付,在后端生成本地支付凭证,模拟支付成功回调。
- 使用测试商户号,需要额外申请,门槛较高。
我在这个项目里用的是模拟支付:用户点击“去支付”后,后端直接生成一个预支付单,小程序端模拟调用支付成功,然后后端将订单状态更新为“待接单”。答辩的时候,只要把逻辑讲通,并且说明在真实环境下会替换成微信支付的能力,老师们一般都能理解。但如果你确实希望接入真实支付,请提前准备好营业执照,并且用微信支付官方文档里的wx.requestPayment接口去对接。
3.5 下拉刷新与分页加载
小程序订单列表和菜品列表如果你不管数据量,一次全部返回也可以,但这样做有两个坏处:一次性加载太多数据导致页面白屏时间长;后期数据量增加后接口响应越来越慢,用户体验会变得很糟糕。所以我在订单列表和菜品列表接口里都做了分页处理,后端统一接收pageNum和pageSize参数,返回用MyBatis Plus的Page对象。
小程序端用onReachBottom来触发加载下一页,配合一个“加载中”的组件提示用户。
javascript复制onReachBottom: function () {
if (this.data.loading || this.data.finished) return;
this.setData({
pageNum: this.data.pageNum + 1,
loading: true
});
this.fetchOrderList();
}
这种分页的方式虽然简单,但很实用。理论上也可以用官方推荐的skyline或者虚拟列表,但对这个项目来说,传统分页已经足够流畅。
4. 商品、订单与后台管理模块实现
4.1 菜品管理模块
管理端后台我使用了一个整合好的后台管理页面,通过Thymeleaf或Vue+Element UI模板来实现。菜品管理模块需要做的事有:
- 菜品的增删改查。新增时上传图片,这里我把图片传到本地服务器的一个
/static/upload目录,然后保存图片访问URL。如果要上线,建议使用对象存储服务(OSS),避免服务器重启丢失图片。 - 分类维护。给菜品设置所属分类,在前端分类栏中显示。
- 库存调整。当菜品卖完后,在后台一键更新库存,或者设置自动停售。
菜品模块的图片上传我碰到过一个比较经典的问题:小程序端使用的图片路径和后台管理端使用的图片URL不统一。比如后端返回的是/static/upload/xxx.jpg,小程序里必须拼上完整域名才能访问;后台管理里自然没问题,因为它是同源部署的。解决方式是在后端统一封装一个图片路径工具类,返回给前端之前就拼好完整URL。
4.2 订单管理模块
后台订单管理界面,我按订单状态做了Tab页签:待付款、待接单、配送中、已完成、已取消。商家进入后台后,默认看到的就是“待接单”这一栏最新订单,方便快速处理。
接单动作要注意处理异常边界,比如商家连续点击接单按钮时,后端要保证接口的幂等性。我在订单表中加了一个版本号字段,在订单状态流转时用乐观锁防止重复操作:
sql复制UPDATE orders SET status = 2, version = version + 1
WHERE id = #{orderId} AND status = 1 AND version = #{oldVersion}
如果更新结果为0,说明订单被别人处理过了,直接返回“订单状态已变更,请刷新页面”。这种处理在多人同时管理后台时非常重要,否则很容易出现重复接单、重复发货的bug。
4.3 数据统计与报表
为了让项目在毕业答辩时有亮点,我在后台管理页面增加了简单的数据看板,使用ECharts展示近7天的订单量和营业额趋势、热门菜品Top10。数据来源就是查询订单表,按日期分组统计。写SQL时要留意时区问题,尤其是ORDER BY和GROUP BY使用DATE(create_time)时,如果数据库连接串的时区设置不对,统计结果会错乱甚至差出8小时。
一条典型的SQL示例如下:
java复制@Select("SELECT DATE(create_time) as day, SUM(total_amount) as total, COUNT(*) as count " +
"FROM orders " +
"WHERE create_time >= #{startTime} AND status = 3 " +
"GROUP BY DAY(create_time)")
List<OrderStatVO> getDailyStat(@Param("startTime") LocalDateTime startTime);
这类SQL在小数据量下没问题,但数据量大之后建议建索引。可以用EXPLAIN查看一下执行计划,create_time字段加普通索引就够了。
5. 项目部署与上线
5.1 本地开发环境搭建
开发部署前,先把环境准备齐全。以下是我自己在本地开发时使用的一套顺手配置:
| 软件 | 版本 | 用途 |
|---|---|---|
| JDK | 1.8 或 11 | SpringBoot 2.7.x建议JDK8+,11更舒服 |
| Maven | 3.8.x | 依赖管理和打包 |
| MySQL | 8.0 | 数据库 |
| Redis | 6.x/7.x | 缓存和登录态,可选 |
| 微信开发者工具 | 最新稳定版 | 小程序开发调试 |
| IDEA | 2022.x+ | Java IDE |
本地启动SpringBoot项目时,可以先把配置文件里的MySQL连接改成jdbc:mysql://localhost:3306/campus_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai,记得在MySQL中创建对应的数据库,然后执行项目内的sql/init.sql初始化脚本。
小程序端在app.js或一个config.js中配置后端地址。本地开发阶段,因为小程序不允许请求localhost,所以需要在开发者工具里勾选“不校验合法域名...”,同时后端地址写成局域网IP,比如http://192.168.1.100:8080。这里我踩过一个坑:如果手机真机调试,需要用电脑的局域网IP,而不是填写http://localhost,否则手机完全无法访问。
5.2 服务器部署方案
部署方案分两种,我推荐先从最简单的开始:
方案一:单机jar包部署(轻量、够用)
- 在项目根目录执行
mvn package -DskipTests,打出一个可执行jar包,一般放在target/目录下。 - 将jar包、部署文档、
sql脚本传到服务器。服务器上安装JDK、MySQL。 - 启动命令参考:
bash复制nohup java -jar campus-order-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=prod \
--server.port=8080 > app.log 2>&1 &
- 如果服务器配置了Nginx,可以配置反向代理,将
/api/路径转发到本地的8080端口,同时处理静态资源。
方案二:Docker部署(适合想练DevOps的同学)
写一个Dockerfile,将jar包打包成镜像,再配合docker-compose一次性把MySQL、Redis、后端服务都启动起来。
dockerfile复制FROM openjdk:11-jre-slim
COPY campus-order-0.0.1-SNAPSHOT.jar /app/app.jar
WORKDIR /app
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
再写一个简单的docker-compose.yml:
yaml复制version: '3.8'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: campus_order
volumes:
- ./mysql_data:/var/lib/mysql
ports:
- "3306:3306"
app:
build: .
depends_on:
- mysql
ports:
- "8080:8080"
environment:
SPRING_PROFILES_ACTIVE: prod
用Docker的好处是环境和开发机完全一致,不会出现“在我电脑上程序明明能跑啊”这种经典问题。但要注意:服务器本身需要能访问外网并且内存足够,不然光MySQL加后端就用掉不少内存。
5.3 部署文档应该写些什么
这套项目通常配有部署文档,很多人忽略这部分的写作,结果答辩被问到时不知道怎么讲,或者别人照着文档部署时反复卡壳。我建议文档至少包含以下几块:
- 环境要求:JDK版本、Maven版本、MySQL版本、微信小程序AppID/AppSecret。
- 数据库初始化:如何创建数据库、导入SQL文件、修改数据库密码。
- 后端配置修改:application-prod.yml里需要修改数据库连接、Redis密码和
wx.appid、wx.secret。 - 构建打包步骤:Maven打包命令、启动命令。
- 小程序端配置:把utils/config.js里的接口地址改成自己的域名或服务器IP。
- 微信公众平台配置:配置服务器域名(request合法域名、uploadFile合法域名),以及业务域名(如果需要跳转网页)。
部署文档写得越清晰,最终提交的效果越好。从评审角度讲,一个能照着跑通的部署文档比写了一堆花哨功能介绍要加分得多。
5.4 HTTPS与合法域名配置
小程序正式上线时需要将后端服务绑定到已备案的域名,并且该域名必须支持HTTPS。这是因为微信小程序对request合法域名的要求是必须为HTTPS。如果只是本地开发和测试,可以在微信开发者工具里勾选“不校验合法域名”;但一旦要用真机预览或者提审小程序,就必须要做域名绑定和SSL证书配置。
推荐做法:在Nginx中配置HTTPS,把证书配好,再配置反向代理到后端jar包服务。以下是一个简化的Nginx配置:
nginx复制server {
listen 443 ssl;
server_name api.example.com;
ssl_certificate /etc/nginx/ssl/api_cert.pem;
ssl_certificate_key /etc/nginx/ssl/api_cert.key;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
证书可以从可信的CA机构申请或使用云服务商提供的免费证书,在微信公众平台后台配置request合法域名时,域名需要经过校验,通常是下载一个校验文件放到域名根目录,或者将指定TXT记录加到DNS解析里。这部分流程如果第一次做,最容易卡在“校验文件放哪”和“TXT记录怎么加”上,文档里建议截图并注明详细步骤。
6. 常见问题与排查技巧实录
6.1 SpringBoot版本太高导致的启动失败
这个是我见过最多的问题。很多同学新建项目时使用了SpringBoot 3.x甚至SpringBoot 3.3,结果依赖里的mybatis-plus-boot-starter还是老版本(比如3.5.1),启动时直接报Invalid value type for attribute 'factoryBeanObjectType': java.lang.String。
解决方式也很简单,两个大方向:
- 升级MyBatis Plus到适配SpringBoot 3的版本,比如从3.5.1升级到3.5.3.1及以上。
- 降低SpringBoot版本到2.7.x,并保持JDK 8或11。
以稳定为上,如果项目不是必须使用高版本特性,建议直接使用SpringBoot 2.7.18,这是2.x系列的最终版本,维护得比较成熟,各种教程和资料也最多,调试问题相对容易。
6.2 小程序获取微信用户信息失败(用户头像昵称获取规则变化)
曾经在很长一段时间内,获取用户头像昵称可以直接用wx.getUserProfile或wx.getUserInfo,但微信后来调整了规则,用户点击授权后拿到的头像昵称会变成默认的灰色头像和“微信用户”,无法直接获取真实信息。
现在的常规做法是:
- 鼓励用户在个人中心主动完善头像昵称,前端使用
button组件开放open-type="chooseAvatar"来选择头像。 - 昵称输入框使用
type="nickname",用户在输入时微信会给出智能推荐。
后端只需要保留一个普通的更新用户信息的接口,把前端上传的头像和填写的昵称存入数据库即可。对校园订餐系统来说,用户能正常下单才是核心,头像昵称是锦上添花,为此纠结太久反而耽误进度。如果需要展示昵称,可以直接显示wx_开头的默认昵称,等用户自己改。
6.3 小程序无法打开公众号文章,需要配置什么
有一类需求是:在小程序里点击某个运营位或者公告,跳转打开一篇公众号文章。这是被问得比较多的问题。这里必须明确:小程序内置的web-view组件打开公众号文章,需要满足几个条件:
- 该公众号必须是已认证的服务号或订阅号,并且与小程序的“主体”一致,且二者已绑定关联。
- 在小程序后台配置业务域名,且只能用
https协议。 - 在公众号后台将小程序添加为关联小程序,并且小程序页面里使用
web-view指向文章链接。
只有满足以上条件,web-view才能正常加载文章。如果主体不一致,大概率会白屏或报错“不支持打开非业务域名”。校园订餐项目一般用不到这种跳转,如果只是想要一个“帮助中心”或者“公告”页面,更简单的做法是直接用后端富文本返回内容,小程序端用一个富文本组件渲染,不涉及跳转问题。
6.4 跨域与CORS的问题
后端接口在前后端联调时,最常见的就是CORS报错。小程序端由于微信内部对网络请求加了限制,一般不会出现传统浏览器那种跨域报错,但如果管理后台是Vue或H5部署在其他端口,后端就必须配置跨域。
解决方式:在SpringBoot里加一个全局CORS配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
注意:如果使用了Spring Security,跨域配置需要在Security的过滤器链中单独放行OPTIONS预检请求,不然还是会被拦截。这类问题在前后端分离架构中非常常见,几乎每位做过前后端联调的人都碰到过。
6.5 用户下单后库存为什么不减
这个问题在答辩时经常被问到。你会发现很多同学做完项目后只测了“正常流程”:用户提交订单、商家接单、完成,库存正常扣减。但一旦用户下单后取消或超时未支付,库存不会恢复。
我在项目里做了两个保证:
- 下单时先锁定库存,不是直接扣减总库存,而是用字段
locked_stock记录锁定数量。 - 支付成功时真正扣减可用库存并清除锁定库存;订单取消时则释放锁定库存。
这样做的好处是,用户在下单到支付的这15分钟内,其他人看到的可售库存是已经减掉锁定量的,避免“显示有货但下单后没货”的尴尬。相关库存恢复逻辑要在事务内实现,并且加锁,防止并发释放时出错。
6.6 时区问题和时间显示错乱
另一个高发问题就是时间显示差8小时。这通常是因为:
- MySQL连接串中没有配置
serverTimezone=Asia/Shanghai。 - SpringBoot中Jackson序列化
LocalDateTime时没有指定时区。 - 服务器或本地操作系统时区不是中国标准时间。
解决方式:在application.yml中统一配置时间格式和时区:
yaml复制spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
数据库连接串也明确指定时区:
yaml复制jdbc:mysql://localhost:3306/campus_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai
如果发现一个页面显示的时间不对,先检查这三处:服务器/本机时区、数据库连接串时区、Jackson序列化时区,八成能解决。
6.7 小程序端页面空白或接口慢
小程序页面空白,最常见原因不是代码错误,而是请求后端接口失败。调试时打开开发者工具的Network面板,看看具体请求状态码和返回内容,就能定位问题。常见的情形有:
- 后端没启动。
- 请求的URL写错了。
- 域名未配置到request合法域名列表。
- 后端接口返回了500,但小程序端没有统一拦截并给出用户提示。
接口慢则可以从SQL层面排查,看看是否有全表扫描或缺失索引。一个典型的例子,如果按用户ID查询订单但user_id没有索引,在订单量上来后查询会非常慢。用EXPLAIN SELECT * FROM orders WHERE user_id = xxx就能立刻看出来。
7. 项目源码与文档整理的几点感悟
最后说说项目代码组织和文档整理。很多同学做课题,代码写完了就完事了,但我强烈建议把项目源码、数据库脚本、部署文档、演示视频整理成一个清晰的目录结构。
我的推荐目录如下:
text复制CampusOrder/
├── backend/
│ ├── src/
│ ├── pom.xml
│ └── sql/init.sql
├── miniapp/
│ ├── pages/
│ ├── utils/
│ ├── app.js
│ └── project.config.json
├── docs/
│ ├── 部署文档.md
│ ├── 接口文档.md
│ └── 演示视频.mp4
└── README.md
README里我会写清楚项目简介、技术栈、如何启动、默认账号密码(如果有)。尤其要说明管理端后台的登录账号,否则别人拿到项目第一关就打不开,体验很糟糕。
还有一个很多人忽略的地方:代码里不要出现敏感信息和硬编码密码。比如数据库密码、小程序AppSecret、支付回调key,应该通过配置文件和环境变量去设置。这样既安全,也给项目留下专业度加成。
从我自己经手的多个类似项目来看,SpringBoot+校园订餐小程序这个组合非常适合作为学习和展示的主线,因为它在业务上“麻雀虽小五脏俱全”,技术上又覆盖了从基础知识到实战部署的全流程。从微信登录、购物车设计到订单状态机再到服务器部署,每一个环节拿出来都值得深入讲解,并且都有充足的可复现路径。
实际去做的时候,我最大的感受是:不要一上来就想着把功能做得又多又花哨,先把“小程序点餐下单、后台接单发货、订单状态正确流转、项目可以稳定部署”这条核心链路跑通,再逐步补充评价、统计、优惠券等扩展功能。很多新手前期把大量时间花在美化界面和调整样式上,结果后端逻辑漏洞百出,最后做出来的项目经不起一两个提问就垮掉了。
如果后续你还想让这个项目再进一步,可以考虑这几个方向:接入真实的微信支付,升级为多商户平台模式(每个食堂窗口都是独立商家),引入消息队列处理峰值订单,或者用Redis缓存热门菜品列表提升接口性能。这些方向在面试或答辩时也都是非常不错的加分项。
我写这篇内容的时候,特意把当年踩过的坑、用的方案、背后考虑的原因都尽量还原了一遍。项目本身不难,但做好每一个细节,让它变得完整、可靠、可维护,并不是一件轻松的事。希望这份整理能帮你把项目的坑提前填平,让你把时间花在真正有意义的地方。
