简介:基于微信小程序的水果销售系统,采用Spring Boot搭建后端、小程序API实现前端交互,覆盖水果浏览、在线购买、订单管理等完整业务流程,是一份适合计算机专业毕业设计、大作业及移动开发实战练习的完整工程资源。压缩包共832个文件,约34.55MB,包含117个java后端源码、145个vue页面组件、80个js逻辑脚本、小程序所需的wxss/wxml样式与页面文件,以及sql数据库结构脚本、论文和开发说明文档,类型齐全,便于对照学习前后端设计与数据库实现。目前已有134人下载学习,资源经导师审定并本地编译调试通过,可稳定运行。附带论文、开发文档和数据库文档,能让学习者快速理解需求分析、系统设计、接口实现等环节,也能够直接部署体验并在此基础上扩展功能,从环境搭建到订单处理均有代码与文档支撑,适合作为课程设计或毕业设计的参考蓝本,可有效提升全栈开发能力。
1. 一套带全文档的水果销售小程序,值不值得照着做
这类「微信小程序 + SpringBoot + MySQL」的水果销售系统,是毕设和外包交付里最常见的组合之一:前端小程序负责商品浏览、加购、下单和订单查看,后端 SpringBoot 提供 REST 接口,MySQL 存放用户、商品、订单数据。看起来功能不复杂,但真正落地时能不能跑通、能不能答辩、能不能被下一个开发者顺利接手,卡点往往不在页面,而在三件事:微信登录态怎么维持、下单时库存怎么不被超卖、源码和论文/数据库文档里的表结构是否对得上。这篇笔记按「拆系统 → 建库 → 后端接口 → 小程序联调 → 文档与避坑」的顺序,把这套交付物从 zip 变成你能复现并改动的东西。适合正在做毕业设计,或想快速入门小程序全栈项目的开发者。
2. 系统拆解:小程序、SpringBoot 与 MySQL 的分工和选型理由
2.1 三层结构各管什么
先给这套系统的整体分工画个边界:
| 层次 | 技术选型 | 核心职责 |
|---|---|---|
| 表现层 | 微信小程序 | 商品浏览、分类筛选、加购、下单确认、订单列表、个人中心 |
| 接口层 | SpringBoot | 登录鉴权、商品查询、购物车同步、订单生成与状态流转、统一返回结构 |
| 数据层 | MySQL | 用户、分类、商品、购物车、订单、订单明细的持久化 |
这套架构里,微信小程序只是「脸」,它不能直接连数据库。常见做法是后端把业务能力暴露成 REST 接口,小程序用 wx.request 调用。你拿到源码时,先在 server 目录找 Controller、Service、Mapper 三层结构,再到小程序这边找 pages 和 utils。为什么要拆这么清楚?因为小程序端如果自己拼数据,首页、购物车、订单的状态会各写一套,改一个页面就要动好几个文件;有了后端,商品价格、库存和订单金额都以数据库为准,端上只负责展示和交互。页面可以随时换,接口保持不变,这个「端上只展示、后端管数据」的原则,就是这个标题背后真正的架构含义。
2.2 为什么 SpringBoot 是这套方案的默认答案
早年间做 Java Web 要用 SSM(Spring + SpringMVC + MyBatis),写接口之前先配一堆 XML,从 web.xml 配到 spring-mvc.xml,配置比业务代码还多。SpringBoot 出来之后,这类项目的搭建成本大幅降低:spring-boot-starter-web 内嵌 Tomcat,跑一个 main 方法就能起服务;mybatis-spring-boot-starter 管数据库访问;所有配置集中在 application.yml。就水果销售系统这种以数据库增删改查为主的 CRUD 项目来说,80% 的重复劳动都被自动配置吃掉了。
对比其他选择:用 Node.js 的 Express 或 Python 的 Flask 也能做,但为什么这个标题选 SpringBoot?原因很现实。一是网上现成的「小程序 + SpringBoot」项目范例最多,照着改最快;二是论文或答辩材料里写「框架选型」这节时,SpringBoot 的陈述最充分,毕竟 Java 生态在企业里最普及;三是后续如果要加管理后台,SpringBoot 接 Vue 或者模板引擎都顺滑。这里特别提醒一句:很多人把 SpringBoot 当成黑匣子,一报错不知道从哪查。先记住三样东西——starter、自动配置、application.yml——基本就能定位大部分问题。另外,网上搜到的大多数教学代码基于 SpringBoot 2.x + JDK8;如果直接套用最新版 SpringBoot 3.x,要先确认 JDK 版本和依赖坐标,后面避坑章节会单独讲版本太高带来的兼容问题。
2.3 登录态与订单状态怎么流转
微信小程序没有传统意义上的 Cookie,登录态不能照搬 Web 那套 session 方案。常见做法是后端签发自定义 token。为什么不用 JWT?JWT 要引依赖、要管密钥和过期时间,对一个小项目来说是额外负担。自定义 token 的逻辑最简单:小程序端 wx.login 拿 code,后端拿 code 去微信换 openid,然后生成一个随机 token 记下 token 和用户的对应关系,返回给前端。后续每个请求的 header 里带这个 token,后端拦截器一查就知道是谁。
// 登录成功后签发 token,并写入内存容器 String token = UUID.randomUUID().toString().replace("-", ""); TokenStore.put(token, user.getId()); // token -> userId R result = R.ok(); result.put("token", token); result.put("userInfo", user); return result;这段代码里的 TokenStore 在示例工程里通常就是一个 ConcurrentHashMap,key 是 token,value 是 userId。它只适合单机演示:SpringBoot 进程一重启,token 全部失效,小程序端收到 401 后重新登录即可,对演示场景完全可以接受。如果要抗多实例部署,把 TokenStore 换成 Redis 就行。前端拿到 token 后存进 wx.setStorageSync,之后每个请求都在 header 里带上;后端拦截器放行 /api/user/login,其余接口都要校验 token,查不到直接返回 code=401,前端请求封装收到 401 后统一清 token 并重新登录。
订单状态我建议用整数状态而不是直接存字符串。常见定义:0 待付款,1 待发货(已付款),2 待收货(已发货),3 已完成,4 已取消。数据库只存数字,接口返回时再转成文字。状态机要锁住几条边界:待付款的订单可以取消,已取消的订单不能流转到待发货,已完成订单不能再做任何修改。把状态流转控制在后端 Service 层,前端只管展示状态文本。
3. 数据库设计与后端接口:建表、登录、下单怎么落地
3.1 六张核心表的结构设计与关系
一套水果销售系统最少需要六张表,再多就容易把毕设做成大而全的航空母舰。我一般按下面这个清单落地:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| user | 微信用户 | id, openid, nickname, avatar, phone, create_time |
| category | 水果分类 | id, name, sort |
| fruit | 水果商品 | id, category_id, name, price, original_price, stock, unit, sales, status |
| cart | 购物车 | id, user_id, fruit_id, quantity, checked |
| order | 订单主表 | id, order_no, user_id, total_price, status, receiver_name, receiver_phone, receiver_address, remark, create_time |
| order_item | 订单明细 | id, order_id, fruit_id, fruit_name, image, price, quantity |
表之间的关系:category 对 fruit 是一对多,user 对 cart 是一对多,order 对 order_item 是一对多。所有关联字段在表里只存 id,不建物理外键约束。为什么?这类交付项目在测试阶段要频繁清库、造数据,物理外键会在删除时各种报错;逻辑外键配合唯一索引已经足够。如果评审老师追问,就在数据库文档里补一句「为避免测试数据维护成本,未启用物理外键约束」。这是一个典型的合理取舍。
order_item 表里存 fruit_name、image、price 三个快照字段,这一点特别关键。商品改名、调价、下架都不能影响历史订单。新建订单时把当前商品的信息原样抄进明细表,之后订单明细永远显示下单那一刻的商品信息。很多初版实现把细节表设计成只存 fruit_id,回查商品表,结果商品下架后历史订单马上显示异常。
3.2 初始化 SQL 脚本与字段参数
拿到源码包后,db 目录下应该有一份 init.sql。如果它和你本地 MySQL 版本不兼容,或者压根没给,就按下面的结构重建:
CREATE DATABASE IF NOT EXISTS fruit_sale DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE fruit_sale; CREATE TABLE `user` ( `id` INT NOT NULL AUTO_INCREMENT, `openid` VARCHAR(64) NOT NULL COMMENT '微信 openid,全局唯一', `nickname` VARCHAR(64) DEFAULT '' COMMENT '昵称', `avatar` VARCHAR(255) DEFAULT '' COMMENT '头像', `phone` VARCHAR(20) DEFAULT '' COMMENT '手机号,可为空', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; CREATE TABLE `fruit` ( `id` INT NOT NULL AUTO_INCREMENT, `category_id` INT NOT NULL DEFAULT 0 COMMENT '分类 id', `name` VARCHAR(64) NOT NULL COMMENT '水果名', `image` VARCHAR(255) DEFAULT '' COMMENT '图片地址', `price` DECIMAL(10,2) NOT NULL COMMENT '现价,单位元', `original_price` DECIMAL(10,2) DEFAULT 0.00 COMMENT '原价,用来展示划线价', `stock` INT NOT NULL DEFAULT 0 COMMENT '库存', `unit` VARCHAR(16) DEFAULT '斤' COMMENT '计价单位,斤/盒/个', `sales` INT NOT NULL DEFAULT 0 COMMENT '销量', `status` TINYINT NOT NULL DEFAULT 1 COMMENT '1 上架,0 下架', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='水果商品表'; -- cart、orders、order_item、category 按同样风格补全几个参数解释一下:
- 字符集必须 utf8mb4,不能用 utf8。MySQL 的 utf8 最多存 3 字节,小程序的用户昵称和订单备注里只要出现 emoji,直接报 Incorrect string value,这是最常见的翻车点之一。
- price 用 DECIMAL(10,2),不用 FLOAT 或 DOUBLE。浮点数在累计金额、对账时会产生精度误差,钱这个东西必须用定点数。
- openid 加 UNIQUE KEY。同一个微信号反复登录时,后端先按 openid 查、查不到再插,唯一索引兜底防止并发下出现重复用户。
- stock 用 INT,配合后续的 UPDATE 原子扣减,业务层必须保证库存不为负。
- status 只做上下架切换,不要物理 DELETE 商品。商品一旦被真删,历史订单明细和收藏数据全部失去关联。
注意:order 是 MySQL 保留字,直接 CREATE TABLE order 会报语法错误。要么把表名改成 orders,要么写 SQL 时始终用反引号括起来。很多初始化脚本跑不起来,就是死在这个点上。
3.3 接口清单与统一返回格式
后端接口按模块划分,整个项目核心就七条:
| 方法 | 路径 | 说明 | 是否需要登录 |
|---|---|---|---|
| POST | /api/user/login | 微信 code 换 token | 否 |
| GET | /api/fruit/list | 商品列表,categoryId=0 查全部 | 否 |
| GET | /api/fruit/detail | 商品详情 | 否 |
| POST | /api/cart/add | 加购 | 是 |
| GET | /api/cart/list | 购物车列表 | 是 |
| POST | /api/order/create | 创建订单,事务内扣库存 | 是 |
| GET | /api/order/list | 当前用户订单列表 | 是 |
接口返回格式统一走一个 R 类:
@Data public class R { private int code; // 0 成功;1 业务失败;401 未登录 private String msg; // 提示信息,前端直接 toast private Object data; // 业务数据 public static R ok() { return ok(null); } public static R ok(Object data) { /* 返回 code=0 */ } public static R error(int code, String msg) { /* 返回失败 */ } }为什么要单独定义 401?小程序端请求封装里看到 code=401 就清除本地 token 并重新走登录流程,而不是把错误提示弹给用户。这样「登录态过期」这个分支只在一个地方处理,页面代码不用每个请求都判断一遍。msg 字段给 wx.showToast 直接用,后端不要返回一长串异常堆栈。
3.4 登录接口与下单事务:核心代码和参数说明
登录接口的简化版长这样:
@RestController @RequestMapping("/api/user") public class UserController { @PostMapping("/login") public R login(@RequestBody Map<String, String> body) { String code = body.get("code"); String openid = wechatService.code2Session(code); // 调微信接口换 openid User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); userMapper.insert(user); } String token = UUID.randomUUID().toString().replace("-", ""); TokenStore.put(token, user.getId()); return R.ok().add("token", token).add("userInfo", user); } }说明几个重点:code2Session 这一步需要 AppID 和 AppSecret,这两个值只能放在后端配置里,绝不允许出现在小程序代码中。小程序端 wx.login 拿到的 code 是一次性的,后端换完 openid 后立即失效,不能缓存。如果按 openid 查不到用户,说明是首次登录,直接 insert 新用户。openid 的唯一索引会在并发场景下兜底,避免同一微信号一瞬间注册两条记录。
下单接口是整个项目里最需要抄对的一段:
@Transactional(rollbackFor = Exception.class) public Long createOrder(Long userId, List<CartItem> items, String receiverName, String receiverPhone, String receiverAddress) { // 1. 生成订单号 String orderNo = "FS" + System.currentTimeMillis() + userId; // 2. 循环更新库存并插入订单明细 BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { int affected = fruitMapper.reduceStock(item.getFruitId(), item.getQuantity()); if (affected == 0) { throw new BizException("库存不足"); } Fruit fruit = fruitMapper.selectById(item.getFruitId()); total = total.add(fruit.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); orderItemMapper.insert(...); // 插入 fruit_name/price/image 快照 } // 3. 插入订单主表并返回订单 id Order order = new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalPrice(total); order.setStatus(0); orderMapper.insert(order); return order.getId(); }这三点必须讲清楚。第一,@Transactional(rollbackFor = Exception.class) 不是可选项。Spring 的事务注解默认只对 RuntimeException 回滚,如果业务抛的是检查异常而 rollbackFor 没写,库存扣了、订单没写入,事务不会回滚,账面上直接出现脏数据。第二,reduceStock 的 SQL 要用原子更新:
UPDATE fruit SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}影响行数为 0 就代表库存不足,抛出业务异常触发回滚。这比「先 select 查库存,判断够不够,再 update」安全得多,因为查和改之间的并发窗口被一条 UPDATE 语句直接堵死,两个用户同时买最后一个也不会超卖。第三,订单总价必须用数据库里的 fruit.price 重新计算,不能信任前端传来的价格。小程序端请求参数是可以被改的,后端必须重新查库校验,防止有人传一个低价把订单金额改掉。
4. 微信小程序前端:登录、页面与请求联调的落地步骤
4.1 页面清单与 tabBar 配置
拿到小程序源码后,先看 app.json 里的 pages 数组。一套完整的水果销售小程序,通常至少包含:首页、商品详情、购物车、订单确认、订单列表、我的。tabBar 保留首页、购物车、我的三个入口,分类和订单都从页面内部进入,tab 太多会把小程序挤得不像电商。
{ "pages": [ "pages/index/index", "pages/category/category", "pages/cart/cart", "pages/order/order", "pages/user/user", "pages/fruit/detail", "pages/order/confirm" ], "window": { "navigationBarTitleText": "鲜果优选", "navigationBarBackgroundColor": "#4CAF50", "navigationBarTextStyle": "white" }, "tabBar": { "color": "#999999", "selectedColor": "#4CAF50", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/index/index", "text": "首页" }, { "pagePath": "pages/cart/cart", "text": "购物车" }, { "pagePath": "pages/user/user", "text": "我的" } ] } }这里有两个细节。list 里的 pagePath 必须和 pages 数组里的路径完全一致,漏一个或写错一个,开发者工具直接报错。tabBar 的 iconPath 不是必填字段,没有图片也能显示纯文字 tab;我建议先把功能跑通,再补 81x81 的 png 图标,避免一开始被图标路径问题卡住。另外,页面用的是系统默认导航栏,所以不需要处理「微信小程序顶部导航栏高度」。如果改成 navigationStyle: custom 自定义导航,不同机型的胶囊按钮位置不一样,得用 wx.getMenuButtonBoundingClientRect 和状态栏高度算顶部占位,那是额外工作量,交付项目里不值得折腾。
4.2 请求封装与登录态维护
小程序端所有接口调用都走同一个封装,utils/request.js:
const BASE_URL = 'http://localhost:8080'; function request(path, method, data) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method || 'GET', data: data || {}, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') || '' }, success(res) { if (res.data.code === 401) { wx.removeStorageSync('token'); login().then(() => request(path, method, data)).then(resolve).catch(reject); return; } if (res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }再把登录逻辑独立成 utils/login.js:
function login() { return new Promise((resolve, reject) => { wx.login({ success(res) { request('/api/user/login', 'POST', { code: res.code }).then(data => { wx.setStorageSync('token', data.token); wx.setStorageSync('userInfo', data.userInfo); resolve(data); }).catch(reject); }, fail: reject }); }); }BASE_URL 本地联调写 localhost:8080,真机预览时必须改成电脑的局域网 IP,比如 http://192.168.1.23:8080,否则手机上的小程序连不上电脑服务。开发者工具右上角的「本地设置」里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」,这个选项只对开发环境有效;上线必须换 HTTPS 域名并在小程序后台配置 request 合法域名。
把 401 的处理统一放进请求封装,是登录态维护里最值得抄的写法。业务页面完全不用关心「什么时候重新登录」,一旦 token 过期或后端服务重启导致 token 丢失,封装层自动清 token、重新 wx.login、重新发起原请求。这不是死循环:重新登录后的新 token 写入 storage,重发的请求用的是新 header。如果小程序要用手机号,用 button 的 open-type="getPhoneNumber" 组件,用户点击授权后拿到 code,再把 code 传给后端换手机号。这一步要求小程序已经完成企业主体认证,个人主体小程序用不了,毕设阶段手机号可以先留空。
4.3 商品列表页与购物车本地缓存
首页 onLoad 时拉取商品列表:
Page({ data: { fruitList: [], loading: false }, onLoad() { this.fetchFruitList(); }, fetchFruitList() { this.setData({ loading: true }); request('/api/fruit/list', 'GET', { categoryId: 0 }).then(list => { this.setData({ fruitList: list, loading: false }); }).catch(() => { this.setData({ loading: false }); }); } })categoryId 传 0 表示查全部分类,后端用 MyBatis 动态 SQL 拼 where 条件。列表接口只返回 status=1 的上架商品,这个过滤必须放在后端做,前端不要自己 filter,否则下架商品会被缓存或转发出去。
购物车这块,我建议「本地缓存为主、后端同步为辅」:
function addToCart(fruit) { const cartList = wx.getStorageSync('cartList') || []; const found = cartList.find(item => item.id === fruit.id); if (found) { found.quantity += 1; } else { cartList.push({ id: fruit.id, name: fruit.name, price: fruit.price, image: fruit.image, quantity: 1, checked: true }); } wx.setStorageSync('cartList', cartList); wx.showToast({ title: '已加入购物车', icon: 'success' }); }本地缓存的好处是加购零等待,不依赖网络,点一下立即有反馈。坏处是同一用户换设备购物车不共享。对水果销售这个场景完全可以接受,毕业设计的说明文档里把边界写清楚就行。结算时把本地购物车已勾选的条目发给 /api/order/create,后端重新查库计算总价和库存,成功后前端把本地购物车中对应条目清掉。
4.4 下单页与订单列表的联调顺序
订单确认页从本地购物车读取已勾选的条目,展示商品图、名称、单价、数量、总价,确认后把 items 和收货人信息 POST 到 /api/order/create。成功后清空本地购物车已下单的部分,wx.navigateTo 跳转到订单列表页。订单列表页在 onShow 里重新调用 /api/order/list,因为每次进入页面时订单状态都可能已经变化,onLoad 只在第一次进入时触发,onShow 才能保证页面每次可见都拉最新数据。状态字段后端返回数字,前端用 map 转成「待付款 / 待发货 / 待收货 / 已完成」的文本,再配上对应的操作按钮。
如果真机预览时出现「页面显示和模拟器不一致」的情况,大概率是机型尺寸或网络环境导致,这种问题有时候像玄学,先看调试器的 Network 面板,再看代码里有没有依赖模拟器才有的接口,这两步能排查掉九成问题。
5. 交付物与避坑:源码、论文、数据库文档怎么检查
5.1 四份交付物怎么组织,不踩格式的坑
标题里带的「源码、论文、说明文档、数据库文档」,是这类交付包的标配。常见做法是解压后分成四个目录:
fruit-sale/ ├── server/ # SpringBoot 后端源码 ├── miniprogram/ # 微信小程序前端源码 ├── db/ # 初始化 SQL 和数据库设计说明 ├── docs/ # 论文、部署说明文档 └── README.md # 启动顺序和环境要求说明文档至少要写清楚三件事:用哪个版本的 JDK、Maven、MySQL;先启动 MySQL 执行 init.sql,再启动 SpringBoot,最后用微信开发者工具导入 miniprogram;小程序端 BASE_URL 在哪里改。论文里的数据库设计章节直接给 E-R 图和字段表,字段表必须和 init.sql 完全一致。「数据库文档和源码对不上」是最常见的翻车点,后面单独一条讲。
5.2 坑 1:小程序请求本地接口直接不通
现象:开发者工具里 wx.request 报「url 不在以下 request 合法域名列表中」,或者真机上报 request:fail。
原因:小程序运行时强制校验域名,本地开发环境没有配置任何合法域名。
解决:开发期在微信开发者工具的「详情 → 本地设置」里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。真机联调时把 BASE_URL 从 localhost 换成电脑的局域网 IP,并保证手机和电脑在同一个 WiFi 下。这里的 http 地址只能用于开发环境,上线必须用 https 域名并在小程序管理后台配置 request 合法域名。
5.3 坑 2:登录态反复失效,每次启动都重新登录
现象:小程序每次冷启动都要先转一圈登录流程,或者请求刷着刷着突然 401。
原因:前端没有把 token 持久化到 storage,每次启动都当新用户;或后端 TokenStore 在进程重启后清空;或 token 有效期被设得太短。
解决:前端保证登录成功后 wx.setStorageSync 写入 token,请求封装里每次从 storage 读取;后端演示期可以把 token 过期时间拉到 7 天,或者干脆不在 token 上加过期时间,用 TokenStore 里的最后访问时间做清理。上线方案是把 TokenStore 换成 Redis,毕业设计里引入 Redis 会增加部署复杂度,说明文档里注明「生产环境需替换为 Redis」即可。
5.4 坑 3:库存被超卖,下单成功但库存变成负数
现象:两个用户同时买最后一个商品,两个人都下单成功,库存变成负数。
原因:典型的并发问题。代码里先 select 查库存,判断大于 0 再 update 扣减,这两步之间存在并发窗口,两个请求同时通过判断,然后各自执行 update。
解决:把「查库存 + 扣减」合并成一条原子 UPDATE,就是第 3 章给出的 reduceStock 写法:UPDATE fruit SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}。影响行数为 0 则抛异常回滚。再用 @Transactional(rollbackFor = Exception.class) 把整个下单流程包起来,库存和订单要么一起成功,要么一起回滚。
5.5 坑 4:SpringBoot 版本太高,依赖和 JDK 不兼容
现象:新建项目时选了最新版 SpringBoot,启动报错,或者引入网上抄来的 PageHelper、旧版 mybatis 后编译直接失败。
原因:SpringBoot 3.x 基于 jakarta.* 命名空间,且默认要求 JDK17;而网上大量「小程序 + SpringBoot」教程和毕设代码还是 javax.* 的 2.x 写法,照抄必然翻车。
解决:做这类交付项目直接锁定 SpringBoot 2.7.x + JDK8 + mybatis-spring-boot-starter,资料最多、兼容问题最少,论文里也最好解释。确定用 3.x 之前先确认手里的依赖坐标全部升级到对应版本。
5.6 坑 5:论文里的 E-R 图和数据库文档跟实际表对不上
现象:答辩时老师按论文字段表去数据库里查,发现字段名对不上,或者某张表在代码里根本没用到。
原因:论文后补,E-R 图和字段表是复制粘贴的,没有按最终改过的 SQL 同步。
解决:把 db 目录下的 init.sql 当成唯一事实来源。表结构改任何一处,先改 init.sql,再同步数据库文档,最后才轮到代码。用 Navicat 或 DataGrip 连上数据库,直接导出实际表结构,反过来核对论文里的字段表。数据库文档不用写成厚厚一本,包含表名、字段、类型、注释、E-R 关系图、几段关键 SQL 就够。记住一个顺序:先改 SQL,再改文档,最后改代码。反过来做,十次有九次要返工。
6. 跑通之后:验证清单、论文组织和下一步演进
项目跑通后别急着写论文,先把验收路径完整走一遍:
| 步骤 | 操作 | 预期结果 |
|---|---|---|
| 1 | 启动 MySQL,执行 init.sql | 六张表全部创建成功 |
| 2 | 启动 SpringBoot | 控制台出现 Tomcat started |
| 3 | 小程序端触发登录 | user 表新增一条 openid 记录,前端拿到 token |
| 4 | 首页浏览商品 | 列表正常展示上架商品 |
| 5 | 加购、提交订单 | 库存减一,orders 表新增一条待付款订单 |
| 6 | 再次查询该商品库存 | 库存等于原值减购买数量 |
论文组织建议这样安排:摘要部分写清楚「小程序端 + SpringBoot + MySQL」的三层结构;系统设计一章给架构图和 E-R 图;核心代码选三段——登录 token 签发、下单事务、库存原子更新;测试一章写接口测试和两个边界场景:库存不足时下单被拒绝、未登录时访问受保护接口返回 401。这三段代码加两个边界场景,足够撑起一篇结构完整的毕设论文。
如果这个项目要往上走一步,把内存 TokenStore 换成 Redis、文件上传换 OSS,再加一个 Vue 管理后台。管理后台打包后可以放进 SpringBoot 的 static 目录统一部署。跨端复用可以考虑 uniapp,但会重写整个页面层,对水果销售这种场景收益不大,不建议为了用 uniapp 而强行迁移。
我自己做这类交付包时吃过一次大亏:先写代码后补数据库文档,结果论文里写了八张表,库里只有六张,答辩时被老师当场问住。后来养成习惯,任何改动先改 init.sql,再更新文档,最后动代码。项目可以小,文档和源码必须一致。这个顺序值得记下来,希望帮到你少走一次我走过的弯路。
本文还有配套的精品资源,点击获取