简介:这是一份基于微信小程序的奶茶店点餐系统毕业设计完整源码包,同时包含后台管理系统与数据库脚本,主要面向计算机相关专业需要完成毕业设计、课程设计或期末大作业的学生。系统覆盖顾客自助点餐、订单提交、后台菜品管理、销售数据查看等完整业务链路,界面美观、操作简单,且代码中加入了详细注释,并配有使用文档说明,即使新手也可以快速理解项目结构,简单部署后即可正常运行。压缩包共481个文件,文件类型涵盖Java后端源码、Vue后台管理页面、小程序JS逻辑、SQL数据库脚本、YML配置文件以及大量png/jpg界面素材等,整体大小仅5.64MB,目录层级清晰,便于按模块检索与学习。目前该资源已有407人学习下载,属于实用性较强的毕设参考项目。对于需要快速搭建可演示、可答辩的高分课设系统的人来说,这套源码提供了从数据库设计到前后端联调的一站式参考方案。
1. 这套「高分毕设」到底在卖什么:三层结构先立住
「基于微信小程序的毕业设计奶茶店点餐微信小程序源码+后台系统+数据库」这套组合,在近几年的毕设选型里几乎成了标准答案。表面看它只是一个点餐小程序,实际拆开是三层:小程序端负责点餐闭环,后台系统负责商品和订单管理,数据库负责把两端的业务数据串起来。三层正好对应答辩老师最爱追问的前端、后端、数据库三段技术栈,所以被当作高分选题不是没有道理。这篇文章适合两类人:一是计算机相关专业要交毕设或课设的学生,二是想搞懂小程序全栈工程怎么组织、愿意照着完整复现一遍的初级开发者。下文按「拆系统 → 跑通小程序 → 对接后台与数据库 → 避坑 → 答辩验收」的路径展开。
2. 先把边界画清楚:小程序端、后台系统、数据库各管什么
很多第一次做全栈项目的同学,拿到源码第一反应是找「核心代码」,然后被目录吓到。我一般会先劝一句:别急着读代码,先把这个系统的边界画出来。小程序端、后台系统、数据库这三块如果不先分清职责,后面任何一个「看着很奇怪的设计」都会让你卡一下午。所谓高分毕设,本质不是代码写得多炫,而是三层结构自洽、每个模块都能说清「为什么存在」。
2.1 小程序端:点餐闭环需要的五个页面
小程序端的职责只有一个:让顾客完成「逛菜单 → 加购物车 → 下单 → 看订单状态」的完整动作。按这个闭环拆页面,常见的结构是五张页面加一组通用组件。
| 页面 | 核心功能 | 依赖的数据 |
|---|---|---|
| 首页 index | 轮播图、公告、今日推荐 | banner、notice、product |
| 点餐页 category | 左侧分类、右侧菜品列表、触底加载更多 | category、product |
| 购物车 cart | 数量增减、勾选结算、清空 | 本地缓存 cart |
| 订单页 order | 订单列表、订单详情、状态跟踪 | order_master、order_item |
| 我的 mine | 登录态、头像昵称、地址管理 | user、address |
页面数量不要贪多,五页已经覆盖了毕设答辩最常问的 CRUD、列表分页、本地缓存、网络请求四个考点。很多高分模板会额外塞一个「会员中心」或者「积分商城」,在我看来性价比很低——页面多了代码量翻倍,功能又和学生场景不贴合,答辩被追问反而露怯。
这里有一个容易被忽略的细节:导航栏高度。如果你用了自定义导航栏,必须在 onLoad 阶段用 wx.getMenuButtonBoundingClientRect() 拿胶囊按钮位置,反推导航栏真实高度。直接写死 44px 的模板在 iPhone 和安卓上都会翻车,刘海屏机型尤其明显。这也是「微信小程序顶部导航栏高度」这个热搜词的来源,适配代码几乎是每套成品源码的标配。
// utils/navbar.js function getNavBarInfo() { const menu = wx.getMenuButtonBoundingClientRect(); const systemInfo = wx.getSystemInfoSync(); return { statusBarHeight: systemInfo.statusBarHeight, navBarHeight: menu.height + (menu.top - systemInfo.statusBarHeight) * 2, menuButton: menu }; } module.exports = { getNavBarInfo };这段代码的核心是menu.top - systemInfo.statusBarHeight这一段:胶囊按钮离状态栏底部的距离,乘以 2 加上按钮自身高度,就是导航栏的总高度。兼容公式网上有各种变体,但原理都是拿胶囊位置做锚点。如果你的成品源码里导航栏适配写的是固定值,建议自己改成这个公式,答辩时这反而是个可以主动讲的「亮点技术点」。
2.2 后台系统:商家视角的管理后台,只做一个角色
后台系统是这套毕设里最容易被低估的部分,也是拉开分数差距的关键。请注意一个定位问题:这里说的后台是给奶茶店老板用的,不是给顾客用的。所以不需要做用户注册、会员体系,角色只有一个「商家」。大多数成品源码的菜单集中在三块:商品管理、分类管理、订单管理。能再加一个简陋的数据统计页,就已经是加分项了。
| 菜单模块 | 主要页面 | 需要实现的能力 |
|---|---|---|
| 商品管理 | 商品列表、新增/编辑弹窗 | 增删改查、上下架、库存修改 |
| 分类管理 | 分类列表 | 排序、启用停用 |
| 订单管理 | 订单列表、订单详情 | 查看明细、流转订单状态 |
| 数据统计 | 看板页 | 今日订单数、今日销售额、订单状态分布 |
为什么要特意强调「只做一个角色」?因为双角色(顾客端 + 商家端)意味着你要处理两套登录、两套权限拦截、两套菜单权限,工作量接近翻倍,而答辩老师并不会因此给你翻倍的分。把商家这一个角色做得干净利落,比做一个半吊子的双角色系统更讨喜。数据统计页也不用上大屏可视化,用后台模板自带的简单卡片加一个按日订单量的折线图就足够,重点是能讲出一句「通过统计发现周末订单量是工作日的 1.5 倍」,这比图表本身更值钱。
2.3 数据库表结构:八张表足够撑起整个系统
数据库设计是答辩提问的重灾区,也是「高分毕设」的含金量所在。一套标准的奶茶店点餐系统,八张表基本就够了:user、category、product、cart、order_master、order_item、address、banner。下面这份表结构是我见过最常见、也最容易讲清楚的版本。
| 表名 | 核心字段 | 设计要点 |
|---|---|---|
| user | id, openid, nickname, avatar, phone | openid 加唯一索引 |
| category | id, name, sort, status | status 控制前台是否展示 |
| product | id, category_id, name, price, img, stock, status, sales | price 用 decimal(10,2) |
| cart | id, user_id, product_id, quantity | 小程序端可不用,后台统计用 |
| order_master | id, order_no, user_id, total_amount, status, pay_time | 订单主表,order_no 唯一 |
| order_item | id, order_id, product_id, product_name, price, quantity | 明细表冗余商品快照 |
| address | id, user_id, receiver, phone, detail, is_default | 一个用户可有多个地址 |
| banner | id, image, link, sort | 首页轮播图 |
两个关键决策值得展开。第一,订单主表和明细表必须分离,一个订单对应多条商品,明细表里要冗余 product_name 和 price 快照;如果商品后面被删除或改价,历史订单依然能看到当时买的是什么、多少钱,这是「快照」的意义。第二,订单状态字段用 tinyint 而不是字符串,1 待支付、2 已支付、3 制作中、4 已完成、5 已取消;数字状态在代码里比较、流转校验都更直接,数据库索引也更小。用字符串的好处是日志可读性强,但调试时完全可以用一个枚举类映射,可读性不损失。
3. 跑通小程序端:登录、点餐、购物车与列表加载的最小闭环
系统边界清楚了,这一章动手跑小程序端。很多同学喜欢从 UI 开始画,堆页面画了两周,接口一个没联。正确的顺序是先打通「登录 → 拿商品 → 加购 → 下单 → 看订单」这条主链路,页面丑一点没关系,链路通了项目就活了一半。下面按一个完整的源码工程目录组织方式来拆。
3.1 工程目录:源码的骨架长什么样
一套规范的小程序源码,目录结构是有共识的:页面、组件、工具函数、接口请求四层分离。最忌讳的是每个页面里直接写 wx.request,那会让接口地址洒满全工程,改一个 baseUrl 要翻十几个文件。
miniprogram/ ├── app.js / app.json / app.wxss ├── utils/ │ ├── request.js # 封装 wx.request 的 Promise 工具 │ └── navbar.js # 导航栏适配 ├── api/ │ ├── product.js # 商品相关接口 │ └── order.js # 订单相关接口 ├── pages/ │ ├── index/ # 首页 │ ├── category/ # 点餐页 │ ├── cart/ # 购物车 │ ├── order/ # 订单列表/详情 │ └── mine/ # 我的 └── components/ ├── product-card/ # 商品卡片 └── order-card/ # 订单卡片这个结构的核心约束是依赖方向:pages 只能调 api,api 只能走 utils/request.js,组件不直接发请求。好处是以后后台接口地址变了,你只需要改一个文件;request.js 里还可以统一塞 token、统一处理 401 跳登录。如果从网上下载的源码里 request.js 是空的,第一件事就是把它补全,这是整个小程序端的底座。
3.2 登录:wx.login 换 openid 这一步别写错
登录是小程序毕设里第一个让人「翻车」的环节。老教程还在教 wx.getUserInfo 直接弹窗拿头像昵称,2021 年之后这套已经走不通了。现在正确的流程是:wx.login 拿 code,code 发给后台换 openid,后台返回一个自定义 token,小程序把 token 存起来,后续请求都带这个 token。
// api/user.js function login() { return new Promise((resolve, reject) => { wx.login({ success: async (res) => { const code = res.code; try { const res = await request.post('/user/login', { code }); wx.setStorageSync('token', res.data.token); resolve(res.data); } catch (err) { reject(err); } }, fail: reject }); }); }这段代码有三个关键点。第一,wx.login 拿到的 code 是一次性的,有效时间很短,只能由后台拿去换 openid,前端不要尝试解析 code 内容。第二,后台拿到 openid 后不要直接把 openid 返回给前端当 token,而是生成一个自定义 token(常见做法是 UUID 或 JWT),openid 只存在后台。第三,token 存到 Storage 后,request.js 里每次请求从 Storage 取并塞进 header,后台拦截器统一校验。
头像昵称这块,现在的合法做法是让用户点一个带open-type="chooseAvatar"的 button 来选头像,昵称则用 input 让用户自己填。不要再用 wx.getUserInfo,答辩时如实说清楚,反而显得你了解微信的隐私政策更新。
3.3 点餐主流程:加购、结算、下单、列表加载更多
点餐页是整个小程序的核心页,常见实现是左侧一级分类、右侧商品列表。这里有两个高频考点:购物车本地缓存、订单金额后端重算。先看购物车,标准做法是把「商品ID → 数量」的映射存进 Storage,而不是把整个商品对象数组塞进去。
// utils/cart.js const KEY = 'cart'; function getCart() { return wx.getStorageSync(KEY) || {}; } function addProduct(productId, quantity = 1) { const cart = getCart(); cart[productId] = (cart[productId] || 0) + quantity; wx.setStorageSync(KEY, cart); } function updateCart(productId, delta) { const cart = getCart(); if (delta === 0) { delete cart[productId]; } else { cart[productId] = Math.max(0, (cart[productId] || 0) + delta); } wx.setStorageSync(KEY, cart); } module.exports = { getCart, addProduct, updateCart };为什么用「ID 到数量」的映射而不是数组?因为加购同一商品时合并非常方便,渲染购物车列表时再根据 ID 向后端请求商品详情。这样做的另一个好处是本地永远只存 ID 和数量,不存价格——价格以服务端为准,避免用户改本地缓存价格造成金额混乱。updateCart里 delta 为 0 时删掉这个键,是防止数量归零后还留一个 0 的脏数据在购物车里。
下单提交的数据结构也很简单:商品 ID 数组加数量、地址 ID。注意前端别把总价传给后台,总价必须由后台根据数据库里的单价乘数量重新计算。看下面的提交代码:
async function submitOrder(addressId) { const cart = getCart(); const items = Object.keys(cart).map(productId => ({ productId: Number(productId), quantity: cart[productId] })); if (items.length === 0) { wx.showToast({ title: '购物车是空的' }); return; } const res = await request.post('/order/create', { items, addressId }); if (res.data.orderId) { wx.setStorageSync('cart', {}); // 下单成功清空购物车 } return res.data; }金额后端重算是整套系统的安全底线。如果前端传总价、后端直接入库,答辩现场老师把请求里的 total 改成 0.01 再提交一次,整个系统的信任就崩了。清空购物车要在接口成功返回之后,不要在下单前清,否则接口失败用户还得重新加一遍,这是交互上的常见血泪经验。
列表加载更多是另一个必考交互。「微信小程序页面列表加载更多」的标准写法是 onReachBottom 触发,配合 page、hasMore、loading 三个参数:
Page({ data: { list: [], page: 1, hasMore: true, loading: false }, async onReachBottom() { if (!this.data.hasMore || this.data.loading) return; this.setData({ loading: true }); const res = await api.getList(this.data.page + 1); this.setData({ list: this.data.list.concat(res.data.list), page: this.data.page + 1, hasMore: res.data.list.length === 20, // 每页 20 条,返回满页视为还有更多 loading: false }); } });hasMore 的判断不依赖总数,而是「这一页返回条数是否等于页大小」——这个细节很关键。很多新手用 total 和已加载数量比较,结果因为后台总数不准导致加载提前结束或无限请求。loading 标志位防止触底事件连续触发导致重复请求,如果列表出现「闪一下又多了两条」的诡异现象,多半是漏了这个标志位。
4. 对接后台系统与数据库:从接口设计到一键导入的建表脚本
小程序端联调的前提,是后台接口和数据库已经立起来。这一章解决三件事:后台框架怎么选、接口按什么约定设计、数据库脚本怎么写得让老师和代码都满意。后台系统在很多源码包里是独立的一份工程,和前端的 miniprogram 目录平级,通常叫 admin 或者 manage。
4.1 后台框架选型:Spring Boot + Vue 是目前最稳妥的组合
毕设后台的常见选型有三个:Spring Boot + Vue、Node.js + Express + Vue、以及老旧的 JSP 项目。我的建议很直接:用 Spring Boot 配 Vue,前端模板直接套一个 Element Admin 的种子工程。理由有三点:答辩老师普遍熟悉 Java 技术栈,遇到问题能和你对话;Spring Boot 的生态成熟,MyBatis 或 MyBatis-Plus 操作 MySQL 的代码量极小;Element Admin 类模板已经把登录、布局、菜单、权限路由做好了,毕设只需要填商品、分类、订单三个业务模块。
很多同学纠结「用 Spring Boot 是不是太重」,其实在毕设场景里这个「重」反而是优势。工作量和答辩风险成正比,你自己从零搭 Spring Security 会踩几十个坑,而模板自带的路由守卫和登录拦截器能直接复用。如果是 Node 后端,代码更轻,但部分老师不熟悉,追问深度反而难以控制。一句话结论:求稳选 Spring Boot,求新可以选 Node,但不要用 JSP。
4.2 RESTful 接口:一张清单说清后台系统暴露什么
接口设计不需要标新立异,RESTful 风格加上统一返回值就是标准答案。下面这套接口清单基本覆盖了奶茶店点餐系统的全部后台能力:
| 方法 | 路径 | 说明 | 关键参数 |
|---|---|---|---|
| POST | /api/user/login | code 换登录态 | code |
| GET | /api/product/list | 商品分页列表 | categoryId, page, pageSize |
| POST | /api/product | 新增商品 | 商品表单 |
| PUT | /api/product/{id} | 编辑商品 | 商品表单 |
| DELETE | /api/product/{id} | 删除商品 | id |
| POST | /api/order/create | 创建订单 | items, addressId |
| PUT | /api/order/{id}/status | 流转订单状态 | status |
| GET | /api/order/list | 订单列表 | status, page, pageSize |
这里一个容易被忽略的坑:微信小程序的 request 请求的 Content-Type 默认是 application/json,所以后台接口一律用 @RequestBody 接收 JSON,不要用表单参数。混用两种参数风格会让联调时出现「后台收到了但字段全是 null」的玄学问题,本质是把 JSON 和 form 混在一起了。
订单创建的后端逻辑是整套系统里最值得写的代码,它体现了「金额不能信前端」的原则。看一个简化版的 Spring Boot Controller:
@RestController @RequestMapping("/api/order") public class OrderController { @PostMapping("/create") public Result create(@RequestBody @Valid CreateOrderRequest req, @RequestAttribute Long userId) { BigDecimal total = BigDecimal.ZERO; for (OrderItemDTO item : req.getItems()) { Product p = productMapper.selectById(item.getProductId()); if (p == null || p.getStatus() != 1) { return Result.error("商品不存在或已下架"); } total = total.add(p.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } OrderMaster order = new OrderMaster(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(1); orderService.create(order, req.getItems(), req.getAddressId()); return Result.ok(order.getOrderNo()); } }三个设计点必须理解。第一,@RequestAttribute Long userId来自登录拦截器解析 token 后塞进去的值,前端不要传 userId,否则任何人都能伪造他人下单。第二,遍历商品时先查数据库取当前价格,累加得到 total,前端传的金额在这个接口里被完全忽略。第三,订单号生成常见做法是 yyyyMMdd 加时间戳加随机位,量级到这个项目根本不需要雪花算法,手写足够。
4.3 数据库初始化:SQL 脚本要能一键执行
数据库脚本是「高分毕设」验收时最容易直接扣分的点。很多源码包里的 SQL 要么没有,要么是零散的 insert 片段。一份好的初始化脚本应该有建库、建表、插入基础数据三部分,并且能从头到尾执行两次不出错。用 Navicat 或命令行执行时,如果脚本里有 DROP TABLE 再 CREATE,重复执行就不会报错,这种写法在答辩演示时特别重要。
CREATE TABLE `order_master` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `user_id` bigint NOT NULL, `total_amount` decimal(10,2) NOT NULL, `status` tinyint NOT NULL DEFAULT 1 COMMENT '1待支付 2已支付 3制作中 4已完成 5已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user_id` (`user_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意三个细节。status 用 tinyint 加 COMMENT,比用字符串更省空间、更易索引,可读性由注释保证。order_no 加唯一索引,防止并发下单生成重复单号。user_id 加普通索引,因为订单列表最常见的查询就是按用户查。字符集统一 utf8mb4,否则奶茶名里的 emoji 表情会存成乱码——这几乎是所有点餐系统的隐藏坑。
测试数据也要一起给。至少插入 10 条奶茶商品、4 个分类、3 个订单(覆盖已支付、制作中、已完成三种状态)、1 个管理员账号。没有测试数据的后台打开就是空荡荡的,演示时现场插入又浪费时间;有数据撑着的页面,截图放进论文里也好看很多。
4.4 本地联调:把 baseUrl 抽成环境变量
小程序端和后台联调时,最土但最有效的做法是把接口地址抽成一个配置文件。不要在每一个 api 文件里写死域名,否则后台从本机挪到服务器的时候,你要全局替换几十处。
// config/env.js const ENV = 'dev'; const BASE_URL = { dev: 'http://127.0.0.1:8080/api', prod: 'https://yourdomain.com/api' }[ENV]; module.exports = { BASE_URL };开发阶段用 127.0.0.1,开发者工具里勾选「不校验合法域名」就能调试。真机预览时,手机访问不到电脑的 127.0.0.1,要把 ENV 对应的地址改成电脑的局域网 IP,比如 http://192.168.1.101:8080/api,手机和电脑连同一个 Wi-Fi 才能通。这一步是无数人卡过的地方:开发者工具里好好的,一上真机就全军覆没,后面避坑章会细说。
5. 避坑:奶茶店点餐小程序最常见的 5 个翻车点与排查路径
这一章把这些年带毕设见过的坑按出现频率排了个序。每一条都是「现象 → 原因 → 解决」的结构,你在复现或改源码时遇到类似问题,可以按这个路径排查。以下问题不分先后,每一个都真实阻断过开发流程。
5.1 购物车刷新就丢:本地缓存和后端该谁存
现象:小程序里加了几杯奶茶,切到别的页面再回来,购物车空了;或者同一台手机两个微信号登录,购物车内容互相串。
原因:新手把购物车直接放在 Page.data 里,这是内存状态,页面销毁就没了。另外,有人把购物车完整商品信息存进 Storage,没有区分用户,多账号登录时必然串。
解决:毕设场景的标准做法是「本地 Storage 为主,后端 cart 表兜底」。本地用前面写的 productId 到数量的 map 存 Storage;下单时明细一次性提交到后端,后端据此创建订单。本地购物车不需要按用户区分,因为 Storage 是跟着设备走的;真正涉及用户归属的是订单,不是购物车。如果你从网上下载的源码把购物车整个数组塞 Storage 且没做合并,建议替换成 map 结构,顺便解决重复添加问题。
5.2 支付环节卡死:没有商户号时的模拟支付方案
现象:界面做到了「去支付」这一步,wx.requestPayment 一调用就报错,或者点击后没有任何反应,订单永远停在待支付状态。
原因:个人主体的微信小程序没有支付权限。真实的微信支付要求企业主体并开通商户号,学生毕设基本拿不到,这是硬门槛,不是代码问题。
解决:把支付做成模拟接口。支付按钮调 PUT /order/{id}/pay,后台校验订单状态后直接把 status 从 1(待支付)改成 2(已支付),记录 pay_time。代码上要加状态校验:
@PutMapping("/{orderId}/pay") public Result pay(@PathVariable Long orderId, @RequestAttribute Long userId) { OrderMaster order = orderMapper.selectById(orderId); if (order == null || !order.getUserId().equals(userId)) { return Result.error("订单不存在"); } if (order.getStatus() != 1) { return Result.error("订单状态不允许支付"); } order.setStatus(2); order.setPayTime(LocalDateTime.now()); orderMapper.updateById(order); return Result.ok(); }这里的状态校验不是多余的:没有它,用户对已取消的订单点支付,订单会从 5 跳回 2,状态机就乱套了。答辩时明确说「这是模拟支付,真实支付需要商户号」,老师不会扣分,反而会认可你理解业务边界。
5.3 request 合法域名与跨域:开发者工具好好的,真机全挂
现象:开发者工具里接口全部正常,点「真机预览」后所有请求都失败,控制台报「不在以下 request 合法域名列表中」。有时候同样的请求第一次能通,第二次失败,像是玄学。
原因:小程序真机环境对 wx.request 有合法域名校验,你本地调试用的 127.0.0.1 或局域网 IP 不在白名单里。开发者工具默认能关掉这个校验,真机调试也能临时绕过,但一旦发布体验版,域名必须是备案过的 HTTPS。
解决:开发阶段在开发者工具的「详情 → 本地设置」勾选「不校验合法域名、web-view(业务域名)、TLS 版本」,真机调试优先用微信开发者工具自带的「真机调试 2.0」。如果后台是本地 Spring Boot,还需要给后台配 CORS,允许小程序所在域名的跨域访问:
cors: allowed-origins: - https://servicewechat.com - http://127.0.0.1 allowed-methods: GET,POST,PUT,DELETE漏配 CORS 的典型症状是开发者工具偶尔能通、真机必挂,因为小程序请求自带 Origin 头,后台不认就拒绝响应。这个配置看似不起眼,却是「后台系统联调能不能顺利走通」的分水岭。
5.4 商品图片全白屏:数据库里存的是 C 盘路径
现象:后台管理里上传的商品图片明明显示正常,小程序端商品卡片却全部裂开,点开图片是「文件不存在」。
原因:后台代码直接把图片存到了服务器本地磁盘,数据库里存的是 C:\fakepath\xxx.jpg 或 C:\Users... 这样的完整本地路径。小程序端请求这个路径当然拿不到,因为那只是后台机器的文件系统路径,不是一个可访问的 URL。
解决:数据库里只存相对路径 /upload/xxx.jpg,后端把 upload 目录映射成静态资源,小程序端渲染时用 baseUrl 拼接完整地址。拼接逻辑抽成一个工具函数:
// utils/image.js function fullUrl(path) { if (!path) return ''; if (path.startsWith('http')) return path; // 已经是完整地址直接返回 return BASE_URL + path; // 相对路径拼上接口域名 }这样以后域名变了只需要改 BASE_URL 一个变量,数据库里的数据不用动。检查你下载的源码,如果商品表的 img 字段存的是反斜杠路径,把它改成 /upload/xxx.jpg 的格式,图片 404 立刻就解决。
5.5 openid 错乱:换台手机登录就串号
现象:A 用户下单,B 用户登录后台能看到 A 的订单;或者两个微信号先后登录,显示的是同一个用户身份。
原因:大概率是用户表没有以 openid 做唯一约束,登录接口每次收到 code 后不管有没有这个用户都 insert 一条新记录,导致同一人产生多条 user 记录,订单归属随机挂到某一条上。还有一个常见变体:把 wx.login 的 code 当成 openid 存了,code 每次都不一样,于是每次登录都生成新用户。
解决:用户表用自增 id 当主键,openid 加唯一索引;登录接口先按 openid 查,查不到才插入新用户:
public User loginByOpenid(String openid) { User u = userMapper.selectByOpenid(openid); if (u == null) { u = new User(); u.setOpenid(openid); u.setNickname("微信用户" + randomSuffix()); userMapper.insert(u); } return u; }先查后插,解决重复创建;openid 加唯一索引,从数据库层面兜底防重。用户名随机后缀是为了让默认昵称不冲突且有点烟火气,答辩演示时两个微信登录,能清楚看到两个不同用户。这个坑不解决,订单归属错乱会在答辩现场直接社会性死亡。
6. 答辩前的一小时自检:把「能跑」变成「能讲清楚」的三步验证
能跑和能答辩之间隔着一道「自检」工序。这道工序不是让你补功能,而是让你把系统里最薄弱的几个点提前暴露出来,准备好说辞。我自己带过的方案里,死在讲不清状态流转上的比死在 bug 上的多得多。下面这套自检流程值得在答辩前完整走一遍。
先过功能清单。数据库脚本从头执行一遍,确认 8 张表全部建出、测试数据完整;小程序端重新登录一次,走完「加购 → 下单 → 支付 → 查看状态」全链路;后台把订单状态从待支付改到已完成,再回到小程序端刷新,确认状态同步。任何一个环节断掉,先修再继续,不要带着断点进答辩。
然后是数据一致性检查。订单状态机的合法流转是:1 待支付可以到 2 已支付或 5 已取消,2 已支付可以到 3 制作中或 5 已取消,3 制作中只能到 4 已完成。写一段小脚本扫库,把非法跳转的订单揪出来:
# check_order_status.py ALLOWED = {1: {2, 5}, 2: {3, 5}, 3: {4}, 4: set(), 5: set()} rows = query("SELECT order_no, status FROM order_master") for order_no, status in rows: # 这里简化成只检查起始状态合法性 # 实际可以把历史状态流转日志也一起查 if status not in ALLOWED: print(f"{order_no} 状态 {status} 不在合法状态集合内")这个脚本的输出如果是空的,说明订单数据是干净的。答辩时主动说一句「我写了脚本检查订单状态机的合法流转路径」,比任何口头描述都有说服力。
最后是演示路径设计。三分钟演示别从首页开始慢慢点,要直接展示跨模块链路:先在后台把一杯奶茶的价格从 12 改成 15,再到小程序端加购这杯奶茶,看到价格已经变成 15,下单支付后回到后台把订单状态改成「制作中」,再切回小程序端订单页看到状态更新。这条链路一次串起后台系统、数据库、微信小程序三块,老师看完基本就问不出「你数据库真的连了吗」这种问题。
说点我的教训。第一次做点餐系统时,我反着来——先写页面后补表结构,结果订单状态改了三次数据库,页面跟着改了四遍,最后一天还在调字段名。现在的习惯是先定状态流转、再定表结构、最后写页面,顺序对了,代码就是水到渠成的事。这套方案值不值得做,答案很明确:技术栈覆盖面广、业务闭环清晰、演示效果好,是毕设里性价比极高的一类题目。但前提是你能把这套自检走完,而不是停留在「模板能跑」。希望帮到你。
本文还有配套的精品资源,点击获取