基于微信小程序的甜品设计——毕设源码实战
说起微信小程序,这两年的处境挺微妙的——你说它饱和了吧,校园里点餐、宿舍里拼单、社团里报名还在满屏用;你说它过气了吧,随便一个本地甜品店、烘焙工作室用小程序做预约点单,单量比APP还稳。我自己带过好几轮毕业设计,甜品主题的小程序选题几乎年年有人选,原因很简单:业务闭环完整、页面承载量适中、后端接口边界清晰,特别适合本科生在有限周期内做出一套逻辑自洽、可演示、可答辩的系统。
这篇文章就围绕微信小程序甜品设计毕设来做完整拆解。不管你是自己选了类似题目,还是拿到一份带源码的项目想二次开发,我都会把这些年实操中踩过的坑、总结出的套路,以及代码层面真正该注意的点,一并讲清楚。源码版本的工程我在本地跑通过,目录结构、接口设计、流程逻辑都会在下文直接拆开来讲。
先说清楚这篇内容解决什么问题:第一,帮你理解一套甜品小程序从需求到落地的全链路设计;第二,把核心流程——点单、购物车、订单、核销、评价——逐个拆开讲透;第三,给出可直接复现的技术方案:前端用微信小程序原生框架,后端配合 Spring Boot 或 Node.js,数据库用 MySQL;第四,整理答辩时导师最爱问的问题和应对思路。
无论你是准备开题的大三学生、正在赶进度的准毕业生,还是想拿这套项目做二次开发的开发者,这篇文章都会给你比源码本身更多的价值。源码只告诉你“怎么跑”,这篇会告诉你“为什么这么设计”。
1. 项目整体设计与选题思路拆解
甜品小程序这个题目,我在学生项目里见过不同的做法。有的做成纯展示型,只有商品列表和详情,点单靠电话预订;有的做成完整电商闭环,会员、积分、优惠券全上。从带毕设的经验看,中间路线最划算:既有电商的完整骨架,又不至于陷入营销系统的泥潭。
为什么甜品适合做小程序而不是 H5 或者 APP?三个原因。
第一,场景匹配度高。甜品消费是典型的“即时决策、附近推荐、到店即取”场景,用户看到图片想下单,下单后希望尽快拿到。小程序免安装、轻量打开、可以拿到地理位置,天然契合“看到—点单—到店自提或配送”的路径。
第二,业务复杂度适中。一张甜品菜单大概20到40个SKU,分类不过三五组,没有服装电商那种尺码颜色多维组合,也没有生鲜电商的库存批次管理。这个复杂程度正好落在一个本科生能掌控的范围内。
第三,答辩展示效果好。小程序可以在微信开发者工具里直接跑,也可以用预览码在手机上演示,考核老师扫码就能看到完整流程。相比之下,纯后端管理系统或者PC网页的现场演示效果会弱不少。
1.1 核心需求拆解
以我手上的这套源码为例,需求可以拆成三端、六个核心模块。
三端是指:
- 用户端(微信小程序):顾客使用,核心是浏览、点单、支付、查看订单、评价。
- 管理端(Web后台):店主使用,核心是商品管理、订单管理、分类管理、数据统计。
- 服务端(接口层):衔接两端,提供 RESTful API,处理鉴权、业务逻辑、数据持久化。
六个核心模块如下:
| 模块 | 用户端能力 | 管理端能力 | 核心表 |
|---|---|---|---|
| 用户模块 | 微信登录、个人中心、收货地址 | 用户列表、状态管理 | user |
| 商品模块 | 分类浏览、商品列表、详情 | 商品增删改查、上下架 | category, product |
| 购物车模块 | 加购、修改数量、结算 | —— | cart |
| 订单模块 | 下单、支付、取消、确认收货 | 订单列表、发货/核销 | orders, order_item |
| 评价模块 | 评价、晒图 | 评价查看/回复 | comment |
| 营销模块(可选) | 优惠券领取、使用 | 优惠券发放 | coupon |
为什么购物车不单独设计一张复杂的用户维度表?因为甜品订单量不大,购物车可以做成用户维度的持久化表,也可以直接放到前端缓存(Storage)里。我手上的源码用的是前端缓存实现,后端没有购物车表。这样做的好处是接口少、逻辑简单;坏处是换设备购物车会丢。毕业设计能演示就好,选前端缓存完全够用。
1.2 技术选型背后的取舍逻辑
先看这套源码的技术栈,再解释为什么这么选:
- 前端:微信小程序原生框架(WXML + WXSS + JS + JSON),不用 uni-app 或 Taro。
- 状态管理:页面级 data + 全局 app.globalData + 本地 Storage。
- 后端:Spring Boot 2.x(我手上这份是 Java 版,也有 Node.js 版)。
- 数据库:MySQL 5.7 / 8.0。
- 鉴权方式:wx.login 获取 code,后端调用微信 code2Session 接口换取 openid,自定义登录态 token 返回前端。
- HTTP 请求:微信小程序自带 wx.request,统一封装到一个 request 工具函数。
很多人纠结:为什么不用 uni-app?一次编写多端发布不香吗?
我的看法是:毕设场景下,原生框架优于跨端框架。原因有三。
其一,微信开发者工具对原生小程序的调试体验最好,编译速度快、报错信息直接指向 WXML/JS 的对应行;uni-app 的报错要经过一层编译链,排查成本更高。
其二,原生框架的文档和社区示例最多。你搜“微信小程序 购物车 左滑删除”,原生写法一抓一大把;换成 uni-app,还要先折算成 uni 的语法和 API 风格。
其三,涉及微信原生 API(登录、支付、订阅消息)时,原生框架直接调用,不需要通过 uni 的封装层。尤其是微信支付,uni-app 的支付流程在某些基础库版本上有兼容问题,排查起来更麻烦。
如果你已经选了 uni-app 也不用慌,整体思路完全一样,API 换成 uni.xxx 就行,下面的业务设计照常参考。
1.3 目录结构:源码工程的骨架解读
我把源码的目录结构重新梳理过一遍,一个标准小程序工程的骨架长这样:
├── miniprogram/ # 小程序前端 │ ├── pages/ │ │ ├── index/ # 首页(商品分类+列表) │ │ ├── category/ # 分类页(部分版本融合在首页) │ │ ├── cart/ # 购物车 │ │ ├── order/ # 订单列表 + 订单详情 │ │ ├── user/ # 个人中心 │ │ ├── login/ # 登录页面(授权引导) │ │ ├── address/ # 地址管理 │ │ ├── comment/ # 评价 │ │ └── search/ # 搜索 │ ├── components/ # 自定义组件(数量选择器、商品卡片等) │ ├── utils/ │ │ ├── request.js # wx.request 统一封装 │ │ ├── util.js # 时间格式化等工具 │ │ └── config.js # 接口域名配置 │ ├── app.js # 全局逻辑 │ ├── app.json # 全局配置(页面注册、tabBar) │ └── app.wxss # 全局样式 ├── server/ # Spring Boot 后端 │ ├── src/main/java/com/xxx/sweet/ │ │ ├── controller/ # 接口控制器 │ │ ├── service/ # 业务逻辑 │ │ ├── mapper/ # mybatis-plus mapper │ │ ├── entity/ # 实体类 │ │ ├── config/ # 配置(拦截器、跨域) │ │ └── common/ # 统一返回结果、异常处理 │ ├── src/main/resources/ │ │ ├── mapper/ # XML Mapper │ │ └── application.yml │ └── pom.xml └── sql/ # 数据库初始化脚本 └── sweet.sql这套结构虽然简单,但每层职责非常清楚。需要提醒的是:很多源码版本的目录命名不统一,你拿到手第一件事不是急着跑起来,而是先看目录,对着上面的表把每块位置搞清楚,后面改需求才知道往哪里改。
2. 数据库设计与核心字段解析
数据库设计是一套毕设项目的根基。我带学生改过太多项目,前端写得花团锦簇,结果一看数据库表结构,要么缺关联关系,要么时间字段用了字符串,要么状态字段没有任何注释——答辩时一句话就被问住了。
这套甜品项目的数据库涉及约10张表,我重点讲几张核心表的字段设计和设计原因。
2.1 用户表 user
CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `openid` varchar(64) NOT NULL COMMENT '微信openid,唯一标识', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `gender` tinyint DEFAULT 0 COMMENT '性别 0未知 1男 2女', `status` tinyint DEFAULT 1 COMMENT '状态 1正常 0禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里最关键的设计是openid 唯一索引。同一个微信号只能对应一个账号,这是微信生态的天然属性。用户表不要自己搞用户名密码——微信小程序没有传统的账号密码概念,身份以微信为准。
时间字段要用 datetime,而且 update_time 要设置自动更新。不少源码在这里偷懒,用 varchar 存时间,后期做订单排序、按天统计时全是坑。
2.2 商品表与分类表
分类表和商品表是经典的一对多关系。
CREATE TABLE `category` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL COMMENT '分类名称,如:慕斯蛋糕、布丁、饮品', `sort` int DEFAULT 0 COMMENT '排序字段,越小越靠前', `status` tinyint DEFAULT 1 COMMENT '1显示 0隐藏', PRIMARY KEY (`id`) ); CREATE TABLE `product` ( `id` bigint NOT NULL AUTO_INCREMENT, `category_id` bigint NOT NULL COMMENT '所属分类ID', `name` varchar(100) NOT NULL COMMENT '商品名称', `subtitle` varchar(200) DEFAULT NULL COMMENT '商品副标题/简介', `main_image` varchar(255) DEFAULT NULL COMMENT '主图URL', `detail` text COMMENT '图文详情(富文本)', `price` decimal(10,2) NOT NULL COMMENT '价格,单位元', `original_price` decimal(10,2) DEFAULT NULL COMMENT '划线原价', `stock` int DEFAULT 0 COMMENT '库存', `sales` int DEFAULT 0 COMMENT '销量', `status` tinyint DEFAULT 1 COMMENT '1上架 0下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;说两个细节。
第一,价格字段用 decimal(10,2),别用 float/double。二进制浮点数在价格计算上会有精度误差,前端展示可能看不出问题,但后端计算总价、做退款时精度损失就会暴露。这条我在看代码时会专门检查。
第二,商品不直接物理删除,更通用的做法是软删除或状态控制。这套源码里没有 delete 字段,直接通过 status 控制上下架,简单够用。
2.3 订单表与订单项表
订单设计是整个项目里最重要的部分,也是答辩必问的核心。表结构如下:
CREATE TABLE `orders` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '业务订单号', `user_id` bigint NOT NULL COMMENT '下单用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额', `pay_type` tinyint DEFAULT 1 COMMENT '支付方式 1微信支付', `status` tinyint NOT NULL DEFAULT 0 COMMENT '订单状态 0待支付 1已支付/待核销 2已核销/已完成 3已取消 4退款', `address_id` bigint DEFAULT NULL COMMENT '配送地址ID,自提可为空', `take_type` tinyint DEFAULT 0 COMMENT '0自提 1配送', `remark` varchar(255) DEFAULT NULL COMMENT '用户备注', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `finish_time` datetime DEFAULT NULL COMMENT '完成时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL, `product_id` bigint NOT NULL, `product_name` varchar(100) NOT NULL COMMENT '商品快照名称', `product_image` varchar(255) DEFAULT NULL COMMENT '商品快照图片', `price` decimal(10,2) NOT NULL COMMENT '商品快照单价', `quantity` int NOT NULL COMMENT '购买数量', `total_price` decimal(10,2) NOT NULL COMMENT '小计金额', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;为什么订单表要拆成 orders 和 order_item 两张表?因为一个订单包含多个商品,如果一张表存,每个商品一行,订单本身的维度——总额、支付状态、收货信息——就要重复存多遍,冗余且容易不一致。
为什么 order_item 要冗余 product_name 和 product_image?因为商品信息会改,名字会变,价格会调。如果订单明细实时去关联商品表,历史订单显示的名称和价格就是“当前”的商品信息,而不是“下单时”的信息。这就是快照思路,电商订单系统普遍这么做。答辩问“为什么商品改价后历史订单不变”,靠这张表就能答。
2.4 购物车为什么放进前端 Storage
强调一下:这套源码的购物车没有后端表,而是用微信小程序的 Storage 存本地。
实现逻辑是:加购时把商品对象(含 id、名称、图片、单价、数量、小计)存到本地缓存的一个数组里,key 设计为cart_${userId}。购物车页面每次 onShow 都从 Storage 重新读取,保证数据最新。
这么设计的理由是——购物车本质上是一个“未提交的意向”,不属于核心业务数据。用户加购了十次都没下单,这些数据存后端价值不大,还增加接口压力。当然,如果后续想扩展“购物车多端同步”,就需要建购物车表重新设计,毕设阶段不必。
注意一个坑:Storage 存的是商品对象数组,如果后台调整了商品单价,购物车里的旧单价不会自动更新。这里的兜底策略是:进入结算页时,前端购物车传给后端下单接口时,后端必须重新从数据库读取价格计算总价,而不是信任前端传来的价格。这个在后端下单逻辑里必须做到,否则用户改一下请求参数就能以任意价格下单。这是安全设计里很重要的一条。
3. 核心功能模块的实操实现
这一章是全文的重头戏。我会把从用户登录到订单完成的核心链路逐个拆开,结合源码里的关键代码段解析,每个环节都说明业务设计原因和实现要点。
3.1 微信登录与自定义登录态
微信小程序登录流程是新手最容易写糊涂的地方。标准流程分两步。
第一步,小程序端调用wx.login()获取临时 code。
wx.login({ success: (res) => { if (res.code) { // 把 code 发给后端 wx.request({ url: `${config.baseUrl}/api/auth/login`, data: { code: res.code }, success: (resp) => { const { token, userInfo } = resp.data.data; wx.setStorageSync('token', token); wx.setStorageSync('userInfo', userInfo); } }); } } });第二步,后端拿着 code 换 openid。在 Spring Boot 里通过 Hutool 或原生 HttpClient 调用微信接口:
// 微信登录凭证校验 String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appid + "&secret=" + secret + "&js_code=" + code + "&grant_type=authorization_code"; // 响应包含 openid 和 session_key拿到 openid 后查表:存在则直接登录,不存在则自动注册新用户。然后生成一个自定义 token(可以用 UUID 或 JWT),存 Redis 或直接返回给前端,后续请求带上 token,后端通过拦截器验证身份。
学生们最爱问的问题:为什么不能直接拿 openid 当 token?因为 openid 是用户的长期敏感身份标识,一旦泄露,别人可以伪装成该用户调用所有接口。自定义 token 相当于“临时门禁卡”,有效期可控,失效了重新登录即可,安全性好得多。
3.2 首页商品展示:分类导航与商品列表
首页整体布局是:顶部搜索框,中间横向分类导航(可用 scroll-view 横向滚动),下方商品瀑布流列表。
分类导航的实现要点:
<scroll-view scroll-x class="category-bar"> <view class="category-item {{currentCategoryId === 0 ? 'active' : ''}}" bindtap="switchCategory">// 计算选中商品总额 calcTotal() { const selectedItems = this.data.cartList.filter(item => item.selected); const total = selectedItems.reduce((sum, item) => { return sum + item.price * item.quantity; }, 0); this.setData({ totalAmount: total.toFixed(2), selectedCount: selectedItems.length }); }, // 修改数量 changeQuantity(e) { const { id, type } = e.currentTarget.dataset; const cartList = this.data.cartList; const index = cartList.findIndex(item => item.id === id); if (index > -1) { if (type === 'minus' && cartList[index].quantity > 1) { cartList[index].quantity--; } else if (type === 'plus') { cartList[index].quantity++; } this.setData({ cartList }); this.calcTotal(); wx.setStorageSync(`cart_${this.data.userId}`, cartList); } }左滑删除是这个页面容易卡壳的地方,通常用movable-view或手写 touch 事件实现。这套源码用的是更朴素的方案:点删除按钮直接弹确认框。说实话,如果是为了毕设,左滑删除可以简化,把精力放在订单流程上更值。
3.4 确认订单与模拟支付
从购物车到确认订单页,核心工作有三块:
- 展示商品清单:从上一个页面传来的商品列表(或从 Storage 重新读取选中的项)。
- 选择配送方式:自提或配送。配送需要关联地址管理。
- 支付方式:毕设项目一般做不到真实微信支付(需要企业主体商户号),源码里普遍做的是模拟支付——前端点击“支付”,弹出确认框,后端把订单状态置为“已支付/待核销”。
模拟支付是个容易被质疑的点。很多学生的代码里支付接口直接改数据库状态,没有任何延时和校验,老师点“支付”的瞬间状态就变了,显得有点假。
我建议至少做成这样:用户点击支付后,小程序端弹出加载动画1.5秒,然后调用后端模拟支付接口;后端校验订单金额、订单归属,更新状态为已支付并记录pay_time,再返回支付成功。这样看起来更有业务流程感,答辩时也能解释为:生产环境这里对接微信支付统一下单接口,毕业设计中用模拟支付替代以规避商户号资质问题。
后端的模拟支付核心代码长这样:
@PostMapping("/api/order/pay") public Result pay(@RequestBody PayRequest req, @RequestHeader("token") String token) { // 1. 校验token得到userId // 2. 查订单,校验订单属于当前用户 // 3. 校验订单状态=0(待支付) // 4. 更新状态为1(已支付),记录支付时间和支付方式 // 5. 扣减商品库存、增加销量(可异步) // 6. 返回支付成功 }3.5 订单状态机的设计
新手常犯的一个错误是:订单状态用if...else在代码里撒得到处都是,改一个状态逻辑要找半天。
正规做法是定义清晰的订单状态机,在代码里用常量或枚举定义状态,并规定允许的状态流转路径:
| 状态码 | 状态含义 | 可流转到 |
|---|---|---|
| 0 | 待支付 | 1(支付)、3(取消) |
| 1 | 已支付/待核销 | 2(核销完成)、4(退款) |
| 2 | 已完成 | 无 |
| 3 | 已取消 | 无 |
| 4 | 退款 | 无 |
后端在状态更新时统一走一个方法,先校验当前状态是否允许流转到目标状态,不允许就抛异常。这样设计的好处是:无论从哪个入口(用户取消、商家发货、系统超时)触发状态变更,都走同一套校验逻辑,不会出现脏数据。
这套源码里有一个地方我单独做过优化。比如用户下单后15分钟未支付自动取消,可以用定时任务扫描超时订单;但更轻量的做法是:用户查询订单时,如果发现订单是“待支付”且创建时间已超过15分钟,前端直接展示“已超时取消”,并调用后端接口把状态更新为取消。后者不用引入消息队列和定时任务,适合学生项目。
3.6 关于真实微信支付,说点实在的
讲真:真实微信支付在毕设里基本走不通。原因很现实——微信支付商户号需要企业资质或个体工商户资质,个人主体的小程序无法申请;而且申请下来还要签约、配置证书、处理回调验签,学生项目耗在这上面不值。
所以毕设里的“支付”几乎清一色是模拟支付。但答辩时老师大概率会问:“如果接入真实支付,你怎么改?”
建议按这个思路回答:
- 前端调用
wx.requestPayment,传入从后端获取的支付参数(timeStamp、nonceStr、package、signType、paySign)。 - 后端调用微信支付统一下单接口,生成预支付交易会话标识 prepay_id,并返回前端所需参数。
- 用户支付成功后,微信服务器会异步回调商户后端接口(notify_url),后端在回调里验签并更新订单状态。
- 注意:支付成功以回调为准,不能只看前端返回结果。前端返回 success 只代表用户完成了支付输入,真正资金到账需要等微信异步通知。
这套回答讲出来,老师就知道你理解真实支付链路,而不是只停留在 demo 层面。
4. 管理端后台:商品与订单管理
一套完整的毕设,不能只有小程序端。管理后台是小程序的“另一半”,也是体现全栈能力的重要部分。源码里管理端是用 Vue + Element UI 写的 Web 页面,连接同一个后端接口。
4.1 管理端功能清单
管理后台的功能不追求大而全,但至少要有这些:
- 数据看板:展示今日订单数、销售额、用户总数、待处理订单等统计卡片,配合 ECharts 画一周销售趋势图。
- 商品管理:商品列表(分页、搜索)、新增/编辑商品(上传图片、填写价格库存)、上下架操作。
- 分类管理:分类的增删改查、排序。
- 订单管理:订单列表(按状态筛选)、订单详情(商品明细、收货信息)、发货/核销操作、退款操作。
- 用户管理:用户列表、查看用户订单记录。
其中订单管理里的核销操作是甜品店场景独有的。自提订单用户到店后,出示订单二维码,商家输入核销码或在后台点“核销”,订单状态从“已支付”变“已完成”。
源码里实现得比较简——后台订单列表有一个“核销”按钮,确认后状态置为2。如果你想做得更有亮点,可以给订单生成二维码,管理员用微信扫码核销。这个功能实现起来不算难,但演示效果很加分。
4.2 图片上传:小程序与后台的通用方案
图片上传是必做的功能。后台商品管理要传图,小程序端用户评价要传晒图。
方案上,推荐直接用云存储或服务器本地存储。源码里用的是简单方案:后端接收multipart/file上传,保存到服务器本地目录,返回可访问的 URL 路径。为了演示方便,后端配置了静态资源映射:
spring: servlet: multipart: max-file-size: 5MB resources: static-locations: file:${upload.path}这样图片 URL 就可以直接通过http://localhost:8080/upload/xxx.jpg访问。部署到云服务器后,把 upload.path 改成服务器目录,图片外链就能正常展示。
小程序端上传图片到后端的代码:
wx.chooseMedia({ count: 3, mediaType: ['image'], success: (res) => { const tempFiles = res.tempFiles; tempFiles.forEach((file, index) => { wx.uploadFile({ url: `${config.baseUrl}/api/common/upload`, filePath: file.tempFilePath, name: 'file', success: (uploadRes) => { const url = JSON.parse(uploadRes.data).data.url; // 把url存入评价图片列表 } }); }); } });一个容易踩的坑:wx.uploadFile的返回结果是字符串,不是对象,必须先JSON.parse再取数据。不少新手在联调时卡在这里,对照着找就能解决。
4.3 后端统一返回与异常处理
读源码时你会发现,所有接口返回格式都是统一的:
{ "code": 200, "message": "success", "data": { ... } }这是后端Result类统一包装的。异常时返回:
{ "code": 500, "message": "库存不足", "data": null }前端 request.js 里对所有响应做了拦截:code 不为 200 时,统一wx.showToast展示后端错误信息,并 reject 掉。这样前端每个页面的业务代码里不用到处写 try-catch,看起来干净很多。
这个设计看起来简单,但价值很大。它保证了前后端联调时错误信息的一致性和可排查性,也是代码工程化的基本要求。答辩问“接口错误怎么处理”,把这一套讲清楚,很难被挑毛病。
5. 前端关键交互与工程细节
接下来讲小程序前端开发里那些“不写就不知道”的细节。这些内容通常不在需求文档里,但直接决定用户体验和项目完成度。
5.1 底部 tabBar 与页面注册
一个甜品小程序通常有四个主 Tab:首页、分类(或购物车)、订单、我的。
// app.json { "pages": [ "pages/index/index", "pages/category/category", "pages/cart/cart", "pages/user/user" ], "tabBar": { "color": "#999999", "selectedColor": "#FF6B81", "list": [ { "pagePath": "pages/index/index", "text": "首页", "iconPath": "images/home.png", "selectedIconPath": "images/home-active.png" }, { "pagePath": "pages/category/category", "text": "分类", "iconPath": "images/category.png", "selectedIconPath": "images/category-active.png" }, { "pagePath": "pages/cart/cart", "text": "购物车", "iconPath": "images/cart.png", "selectedIconPath": "images/cart-active.png" }, { "pagePath": "pages/user/user", "text": "我的", "iconPath": "images/user.png", "selectedIconPath": "images/user-active.png" } ] } }tabBar 的 iconPath 有两个注意点:图片需要是 PNG 格式,且大小一般不超过 40kb;图标尺寸建议在 81px * 81px 附近。超过大小限制,小程序后台会提示上传失败;图标带不透明底色,在 tabBar 里可能显示成黑色方块。这些坑都是自己踩过才知道的。
5.2 顶部导航栏适配
微信小程序的顶部导航栏高度不是固定的。不同机型状态栏高度不同,刘海屏、灵动岛这些机型状态栏更高。如果用自定义导航栏,不做适配,标题就会顶进状态栏。
源码里的适配方案是:
// 获取状态栏高度 const systemInfo = wx.getSystemInfoSync(); const statusBarHeight = systemInfo.statusBarHeight; // 自定义导航栏高度 = 状态栏高度 + 44px(默认导航栏高度)在 WXML 里:
<view class="nav-bar" style="padding-top: {{statusBarHeight}}px; height: {{navHeight}}px;"> <text class="nav-title">甜品小店</text> </view>这套适配方案在绝大多数安卓/iOS 机型上都能正常显示。如果直接用默认导航栏,这些都不用操心;但用了自定义导航栏,这是必须处理的第一件事。
5.3 列表加载更多与下拉刷新
商品列表、订单列表都需要“上拉加载更多”和“下拉刷新”。“加载更多”的实现要点是分页参数管理:
data: { page: 1, pageSize: 10, hasMore: true, productList: [], loading: false }, // 页面触底时加载下一页 onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.loadProducts(); }, async loadProducts() { const { page, pageSize, productList } = this.data; this.setData({ loading: true }); const res = await request.get('/api/product/list', { page, pageSize, categoryId: this.data.currentCategoryId }); const list = res.data.records; this.setData({ productList: page === 1 ? list : productList.concat(list), hasMore: res.data.pages > page, page: page + 1, loading: false }); }这里的关键是concat而不是赋值——分页加载是在原有列表上追加,不是替换。hasMore的判断依据是当前页是否小于总页数。后端用 MyBatis-Plus 的分页插件,返回的数据结构自带records、pages、total这些字段,前端直接取用即可。
下拉刷新在页面 json 里声明:
{ "enablePullDownRefresh": true, "backgroundTextStyle": "dark" }然后在onPullDownRefresh里把 page 重置为 1,重新请求第一页数据,数据回来后调用wx.stopPullDownRefresh()关闭刷新动画。不少新手写完下拉刷新忘记调 stopPullDownRefresh,加载动画一直转,这个问题很好排查。
5.4 微信开发者工具的使用与真机调试
源码拿到手,第一件事是用微信开发者工具导入项目。具体步骤:
- 打开微信开发者工具,选择“导入项目”。
- 目录选择源码里的 miniprogram 目录(如果前端和后端分开,就选前端目录)。
- AppID 可以先用测试号,也可以注册自己的小程序账号获取。
- 导入后在“详情 - 本地设置”里勾选“不校验合法域名”——本地开发时接口是 http://localhost:8080,微信默认只允许 https 和已备案域名。
- 在
utils/config.js里把 baseURL 改成你本机的局域网 IP,例如http://192.168.1.102:8080,这样手机预览时也能访问到电脑上的后端。
真机预览的坑:手机和电脑必须在同一个局域网内,且手机访问电脑 IP 时不能被防火墙拦截。Windows 系统记得在防火墙里放行 8080 端口,否则手机扫码预览后所有接口全部失败,报错是request:fail。这个在带学生时遇到过太多次,第一次碰到会非常困惑——开发者工具里一切正常,真机就全挂。
6. 二次开发指南:拿到源码后该怎么做
很多读者拿到的是压缩包,解压后面对一堆文件无从下手。我把拿到源码后最高效的上手路径分成七步,每一步都说明目的和容易踩的坑。
6.1 步骤一:环境准备与项目导入
在开始之前,先把环境装齐。我列一个清单:
| 组件 | 版本建议 | 用途 |
|---|---|---|
| JDK | 1.8 或 11 | 运行 Spring Boot 后端 |
| Maven | 3.6+ | 管理后端依赖 |
| MySQL | 5.7 / 8.0 | 数据存储 |
| Redis(可选) | 5.0+ | token 缓存(不用可跳过) |
| 微信开发者工具 | 稳定版 | 运行小程序前端 |
| Node.js(可选) | 14+ | 如果后端是 Node 版 |
后端导入 IDE 后,先修改application.yml里的数据库连接信息:数据库名、用户名、密码。然后用 Navicat 或命令行执行sql/sweet.sql,把表结构和初始数据导入 MySQL。
启动后端前需要注意:如果源码里配置了 Redis 缓存登录态,而本机没装 Redis,启动会报错。解决方法是先启动本地 Redis,或者把缓存逻辑改成 ConcurrentHashMap 的内存缓存——毕设项目够用。改内存缓存的代码量不大,不需要引入 Redis 依赖,很多学生版本就是这么做的。
6.2 步骤二:跑通接口联调
后端启动后,先用 Postman 或 Apifox 测一个最简单的接口,比如GET /api/product/list,能返回商品列表 JSON,说明后端 OK。
然后改前端utils/config.js里的 baseURL:
// 开发环境 const baseURL = 'http://localhost:8080'; // 真机预览时改成局域网IP // const baseURL = 'http://192.168.1.102:8080';在开发者工具里把“不校验合法域名”勾上,刷新小程序,看到首页商品列表,前后端就打通了。
如果接口返回 401 或登录失败,大概率是app.js的 onLaunch 里登录逻辑没跑通。在 Network 面板里看/api/auth/login的请求是否返回了 token,以及后续请求头里是否带上了 token。这里推荐直接用开发者工具的 Network 面板,比自己在代码里打 console.log 高效得多。
6.3 步骤三:改造需求的优先级建议
如果你不想只做“开题答辩版”,想把项目改成更有辨识度的作品,建议按以下优先级改造:
P0(建议改):
- 把默认商品数据换成你自己设计的甜品品牌、商品、文案。
- 更换小程序界面主色调和 Logo,让项目有视觉辨识度。
- 修改
app.json里的导航栏标题和window背景色。
P1(推荐加):
- 增加“今日推荐”或“人气榜单”模块,在首页加一个横向滑动区域。
- 增加订单倒计时显示(下单后 15 分钟未支付自动取消的倒计时)。
- 给订单增加二维码核销功能,后台扫码完成核销。
P2(有余力再加):
- 接入优惠券模块:注册送券、下单用券,后端加优惠券表和用户券表。
- 接入图表统计:用 ECharts 展示近 7 天的销售趋势,管理端数据看板立刻专业起来。
- 消息推送:下单成功后通过订阅消息给用户发送订单状态通知。
从毕设答辩角度讲,P0 决定作品完整度,P1 决定作品亮点,P2 决定作品深度。建议至少完成 P0 全部和 P1 的一到两项。
6.4 如何让答辩演示更出彩
这是很多人忽视的部分。源码能跑只是及格分,演示得好不好直接关系到最终成绩。我的经验是:
演示前准备一份脚本。按顺序展示以下场景:
- 注册/登录:进入小程序自动登录,展示用户信息。
- 浏览商品:首页分类切换、商品列表滚动、查看商品详情。
- 加入购物车:修改数量、选择商品、查看合计金额。
- 提交订单:选择配送方式(自提/配送)、填写备注、提交订单。
- 模拟支付:展示支付确认、支付成功、订单状态变化。
- 切换管理后台:展示订单列表,核销刚才的订单。
- 回到小程序:刷新订单列表,状态变为已完成;补一条评价。
这个脚本不只演示功能,更重要的是展示数据联动的完整性。订单在小程序端下单、后台核销、前端状态同步变化,老师会直观感受到这是一套完整系统,而不是静态页面。
答辩时把数据库表结构准备好。如果老师问到订单设计,能快速打开 Navicat 展开 orders 和 order_item 表,讲清楚主外键关系和快照字段。这种“手上真有东西”的状态比对着 PPT 念强十倍。
7. 常见问题排查与避坑汇编
下面这部分是我这几年带学生做毕设时遇到的高频问题汇总。这些问题我在源码调试过程中几乎都遇到过,每一条都是拿时间换来的经验。
7.1 微信开发者工具报错解析
错误1:app.json: 未找到 app.json 或文件内容格式错误
原因:导入项目时目录选错了。小程序工程目录必须直接包含 app.json,而不是选到整个项目根目录(根目录里可能有前端和后端两个文件夹)。
解决:重新导入,目录选择miniprogram文件夹。
错误2:wx.request 请求失败 request:fail
原因:域名未配置或本地网络不通。开发环境下先在“详情 - 本地设置”勾选“不校验合法域名”;如果是真机预览,检查手机和电脑是否同一局域网、防火墙是否放行端口。
错误3:TypeError: Cannot read property 'data' of undefined
原因:后端返回格式和前端预期不一致。最常见的是后端返回了{code:200, message:'success', data:{...}},前端却写了res.data.data而实际结构不同,或者接口异常返回了空对象。解决办法是在 Network 面板里看实际响应结构,再调整取值路径。
错误4:Failed to load image
原因:图片路径是http://localhost:8080/upload/xxx.jpg,开发者工具能访问但真机访问不了——localhost 在手机上指向手机自己。解决:把后端图片访问地址换成电脑局域网 IP,或部署到云服务器后使用公网域名。
7.2 后端常见异常
异常1:Access denied for user 'root'@'localhost'
原因:MySQL 账号密码不对,或远程访问权限没开。先在命令行用 MySQL 客户端测试连接:mysql -uroot -p,能进说明密码没问题,再检查 application.yml 里的配置是否写对。
异常2:Table 'xxx' doesn't exist
原因:数据库脚本没执行,或连错了数据库。检查 MySQL 里是否有对应数据库和相关表。
异常3:端口被占用
Spring Boot 默认端口 8080,如果本机已有服务占用,启动会报Port already in use。解决:改application.yml的 server.port,或关掉占用进程。
异常4:拦截器导致所有接口 401
这是新手极易忽略的:后端写了登录拦截器,但没有放行/api/auth/login和商品列表等公开接口,前端的 token 还没拿到,其他所有请求全被拦截。
解决:在拦截器配置里明确放行白名单,比如:
registry.addInterceptor(loginInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/auth/login", "/api/product/list", "/api/upload/**");7.3 数据库数据不一致问题
问题1:下单后库存没扣
检查后端下单逻辑里是否校验库存并且执行了扣减。很多源码只在做了前端“库存不足”的提示,后端却没有对应逻辑。需要确认orders表状态更新和product.stock扣减是否在同一个事务里。
问题2:取消订单后库存没回补
和上一条配套:订单取消或退款后,应该把库存加回来。源码里如果没做,建议补上,这是答辩可能被追问的点。
问题3:订单金额和商品价格对不上
原因可能有两个:一是前端传给后端的金额被直接信任,二是商品改价后旧订单明细还是旧价格。正确做法:下单时后端从数据库读取商品最新价格,重新计算总价,并把快照写入 order_item。
前端传的金额只作为展示参考,不作为计价依据。这是我在代码审查时必讲的一条。
7.4 用户体验细节自查清单
在提交之前,用这个清单快速自查一遍,能避免很多低级扣分项:
- 支付按钮是否防止了重复点击?(用 isLoading 置灰)
- 购物车为空时是否展示了空状态文案,而不是白屏?
- 商品库存为 0 时,是否置灰“加购”按钮或提示“已售罄”?
- 网络请求失败时是否有 toast 或错误提示,而不是默默失败?
- 提交订单成功后是否清空了购物车里已下单的商品?
- 订单列表下拉刷新后,状态是否及时更新?
- 商品详情页返回时列表滚动位置是否保留?
最后这一条很多人会忽略。微信小程序的navigateTo跳详情,返回时页面栈里的上一个页面 onShow 会自动触发,列表位置默认保留。但如果你用了redirectTo或reLaunch把上一个页面关了,返回时就会重新加载列表、丢失滚动位置。所以页面跳转选择navigateTo(保留页面栈)还是redirectTo(关闭当前页),要结合交互需求想清楚。
8. 从毕设到真实项目:扩展方向的个人思考
前面聊的都是如何把甜品小程序做成一个合格的毕设。如果你拿到源码后不只是为了交差,而是真的想把小程序做成上线产品,有几个方向值得琢磨。
第一,从“能演示”到“能运营”。真实运营需要数据埋点能力,统计每个页面的访问量、转化率,跑通从“加购”到“支付”的漏斗数据。毕设项目基本没有埋点,上线前需要接入数据分析平台或自建日志体系。
第二,从“模拟支付”到“真实支付”。前面说过,真实支付需要企业主体小程序和微信支付商户号。走上了线这条路,这步绕不开。微信支付 V3 的接入流程可以提前熟悉:申请商户号、配置 API 证书、后端集成 SDK、处理回调。流程不难,但比较繁琐。
第三,从“手动核销”到“自助取货”。思路很简单:订单支付成功后在订单详情页生成取货码或二维码,用户到店后商家扫码确认。这样可以避免高峰期人工核对订单号的低效。再进阶一点,可以结合蓝牙打印机自动打印小票——这也是很多甜品店真实使用的场景。
第四,从“单店”到“多店”。如果单店模式验证成功,下一步是复制到多个门店。多店的核心是增加 store 表、商品与门店关联、库存按门店隔离、订单绑定门店,以及自提点逻辑。架构上其实是从单体应用走向多租户改造,复杂度会明显上一个台阶。
第五,从“小程序”到“私域运营”。甜品店拼的不是一次性流量,而是复购和客单价。小程序配合会员体系可以做得很深:储值卡、积分商城、生日优惠券、社群裂变。这些功能落到后端无非是几张表和几个接口,但产品逻辑的差异很大,值得单独开篇细聊。
我的态度是:毕设只是起点,源码只是脚手架。真正值钱的,是你通过这些代码理解了一整套电商业务流转的能力——从用户登录,到商品展示,到下单支付,再到订单履约。这套逻辑放到任何一个小程序商城上,本质上都是一样的。
9. 实操总结与个人经验分享
做完这么多轮甜品小程序项目,我最深的体会是:毕设项目不追求技术多新颖,追求的是逻辑自洽、流程完整、表达清楚。一套系统哪怕只用了增删改查,只要每个环节都能自圆其说,每个表设计都有理由,每个接口都有异常处理,就能拿到不错的成绩。
具体到甜品小程序这个题,有几个建议送给正在做或准备做的你。
第一,把精力优先放在订单流程上,而不是界面的花哨程度。订单状态流转是这套系统的核心命脉,演示时订单能流畅走完“下单—支付—核销—评价”的全链路,比十个酷炫动画都管用。
第二,数据库表设计要能讲出理由。答辩时老师问“为什么订单明细要存商品快照”,你如果能从历史数据一致性的角度回答,再顺手展开到电商系统的通用设计,这是很亮的加分点。
第三,遇到 bug 先看 Network 面板,再 console.log。微信开发者工具自带的调试器非常强,请求状态、响应数据、控制台报错一应俱全。把请求流程理清楚,90% 的问题都能定位。
第四,不要盲目加功能。很多学生喜欢在毕设里堆功能,优惠券、积分、秒杀、直播全想上,结果每个模块都只做了一半,演示时漏洞百出。与其多而杂,不如少而精。一套甜品点单系统,把商品、购物车、订单、评价四个模块做扎实,已经是一份非常完整的毕设了。
第五,答辩 PPT 里放一张系统架构图。小程序端、后端、数据库三层的架构画清楚,标注好请求流向,老师一看就知道你脑子里有全局图景。这比贴一堆代码截图有效得多。
最后再分享一个小技巧:做演示前,把数据库里的初始数据改成你自己的品牌内容。比如把商品名改成“芒果千层”“提拉米苏”“杨枝甘露”,把店铺名改成你自己起的名字,把公告改成欢迎语。这样哪怕代码是参考的,演示效果也是属于你自己的作品。不少学生忽略了这一步,评委看了多年的“甜品小店”,一眼就能看出是模板套的。
源码会给你一条已经铺好的路,但你应该在路上留下自己的脚印。把项目理解透、改顺了,它就不再是别人的源码,而是你自己的作品。