奶茶店点餐小程序全栈开发实战:Spring Boot与MySQL深度解析

发布时间:2026/10/22 9:13:59
奶茶店点餐小程序全栈开发实战:Spring Boot与MySQL深度解析 简介一套完整可运行的高分毕业设计项目面向计算机相关专业学生聚焦奶茶店点餐场景。包含用户端微信小程序、后台管理系统与数据库三部分覆盖商品浏览、下单支付、订单管理、数据统计等核心业务功能完善界面清爽适合作为毕业设计、期末大作业或课程设计参考。压缩包共481个文件以Java后端服务、Vue管理页面、JavaScript小程序逻辑为主另含SQL数据库脚本、YML环境配置以及大量PNG/JPG界面截图整体仅5.64MB目录结构清晰部署简便。源码附有详细注释和使用文档新手也能快速上手系统经过严格调试可稳定运行后台支持商品分类、订单状态、用户信息等常见管理操作具备良好实用价值。已有407人浏览学习对需要完成类似课题的学生有较高参考价值。1. 奶茶店点餐小程序为什么这个毕设方向值得做每年答辩季都会看到一批奶茶店点餐小程序但真正拿高分的永远是少数。差异不在页面好不好看而在有没有把点餐、加购、下单、后台接单、订单状态流转这条链路完整跑通。这个标题里的源码 后台系统 数据库三件套正好对应了毕设评审最看重的三层能力小程序端做出真实点餐体验后台系统管住商品和订单数据库把用户、商品、订单之间的关系理清楚。选题不算新颖但胜在业务闭环清晰、演示效果好、答辩有得讲。如果你手上正好需要一套能复现、能改、能讲的奶茶店点餐全栈项目这篇文章从技术选型讲到表结构和踩坑记录照着落地就行。2. 技术选型与架构分工前后端分离、云开发怎么选2.1 三种主流技术方案对比云开发、Spring Boot 分离、Node 轻量后端先说结论奶茶店点餐小程序这个题目做的人十有八九卡在第一步——不知道后端到底该用哪种方式。市面上常见的方案有三种各有各的坑。第一种是微信云开发。小程序端直接用云函数、云数据库不需要自己买服务器也不需要管域名备案。好处是快一个熟悉 Vue 的同学两周就能把菜单、购物车、订单全串起来。坏处是后端逻辑全在黑匣子里答辩时老师问订单表怎么设计的并发情况下库存怎么控制你只能说云函数里写的。云开发适合时间紧、重点想展示前端能力的场景但拿高分很难——因为毕设评审看的就是你对系统整体的把控力。第二种是 Spring Boot MySQL 前后端分离这也是大部分高分毕设的经典套路。小程序端做展示和交互后端拆成 Controller、Service、Mapper 三层管理端再套一个简单的 Web 页面。好处是架构清晰答辩时能画请求流转图能讲清楚每一个接口背后的数据操作。坏处是环境准备成本高你得会装 JDK、Maven、MySQL还得会部署。如果你本身就是计算机科班数据库课设做过类似的东西我建议直接选这个性价比最高。第三种是 Node.js Express或 Koa MySQL。代码量比 Spring Boot 少一半接口写起来快前端同学也能上手。坏处是工程感弱一些事务、拦截器这类东西没有 Spring 体系那么现成。适合后端基础一般、但想在答辩里展示出我确实写了一个服务端的人。三个方案对比看这张表方案开发速度后端可见度部署成本适合谁微信云开发快约 2 周低逻辑在云函数最低不需要服务器时间紧、前端为主Spring Boot MySQL中等约 46 周高分层清晰需要云服务器 域名备案科班、冲高分Node.js Express中等约 34 周中能讲清但工程感弱同左后端经验一般我的判断是如果这篇博文面向的是高分毕设那默认走 Spring Boot MySQL 这条最稳的路。后面所有代码示例也按这个组合来写。2.2 小程序端目录结构与全局配置从 app.json 搭起骨架不管后端选什么小程序端的结构是差不多的。拿到源码后第一件事不是急着看页面代码而是先打开 app.json理解这个小程序有哪些页面、底部导航怎么配、全局窗口样式是什么。{ pages: [ pages/index/index, pages/cart/cart, pages/order/list/list, pages/user/user ], window: { navigationBarBackgroundColor: #ffffff, navigationBarTitleText: 奶茶点单, navigationBarTextStyle: black, backgroundColor: #f5f5f5 }, tabBar: { color: #999999, selectedColor: #e74c3c, list: [ { pagePath: pages/index/index, text: 菜单 }, { pagePath: pages/cart/cart, text: 购物车 }, { pagePath: pages/order/list/list, text: 订单 }, { pagePath: pages/user/user, text: 我的 } ] }, style: v2, sitemapLocation: sitemap.json }pages 数组第一项是启动页tabBar 的 pagePath 必须和 pages 里一一对应。这里有个容易忽略的点tabBar 页面之间跳转要用 wx.switchTab不能用 wx.navigateTo后者会报错。购物车、订单列表这类页面放在 tabBar 里用户点完餐能随时切过去看状态演示体验会好很多。全局样式一般在 app.wxss 里定义常见做法是把主题色、通用按钮样式、卡片阴影放在这里页面里就不用重复写。小程序不像 H5 那样有 CSS 变量兼容性问题直接在 app.wxss 里用 page 选择器设置背景色和基础字体即可。2.3 一个能跑的请求封装统一处理登录态与错误提示小程序原生 wx.request 写起来很啰嗦每个页面都写一遍 success/fail 回调代码会很快失控。我习惯的做法是封装一个 request.js把 baseURL、token 注入、错误提示、登录态失效跳转全部收敛到一个文件里。// utils/request.js const BASE_URL https://api.你的域名.com const TOKEN_KEY token function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: ${BASE_URL}${path}, method, data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(TOKEN_KEY) || }, success(res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else if (res.statusCode 401) { // 登录态失效踢回登录页 wx.removeStorageSync(TOKEN_KEY) wx.navigateTo({ url: /pages/user/user }) reject(res) } else { wx.showToast({ title: 请求失败, icon: none }) reject(res) } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) } module.exports { request, BASE_URL }这段代码有三个关键设计。第一所有请求自动带上 Authorization 头后端从 token 里解析用户身份页面代码不用关心登录态怎么传。第二401 统一处理后端返回登录失效时前端自动清掉本地 token 并跳到用户页不需要每个接口都重复判断。第三业务页面只关心 res.data 里的业务数据——后端返回格式统一为{ code: 0, message: ok, data: ... }请求层把 HTTP 状态码和业务状态码分开处理排错时一眼就能看出是网络问题还是业务问题。注意 BASE_URL 我写的是示例域名实际开发时如果还没买域名可以先在小程序开发者工具里勾选不校验合法域名用 HTTP 调试后面部署再换成 HTTPS。这个点后面避坑章节还会重点讲。3. 小程序端点餐主流程菜单、购物车、下单与订单查询3.1 菜单页左侧分类、右侧商品的布局与数据绑定奶茶店点餐的界面范式很固定左侧是奶茶品类原味茶、鲜奶茶、果茶、小料右侧是当前分类下的商品列表每件商品展示图片、名字、价格和一个加购按钮。这个布局用小程序原生的 scroll-view 就能实现不需要任何第三方组件。!-- pages/index/index.wxml 核心结构 -- view classmenu-page scroll-view classcategory-bar scroll-y view wx:for{{categories}} wx:keyid classcategory-item {{activeIndex index ? active : }} bindtapswitchCategory>// pages/index/index.js const { request } require(../../utils/request) Page({ data: { categories: [], products: [], activeIndex: 0 }, onLoad() { this.loadCategories() }, loadCategories() { request(/api/category/list).then(res { this.setData({ categories: res.data }) this.switchCategory(0) }) }, switchCategory(e) { const index typeof e number ? e : e.currentTarget.dataset.index this.setData({ activeIndex: index }) const categoryId this.data.categories[index].id request(/api/product/list?categoryId${categoryId}).then(res { this.setData({ products: res.data }) }) } })这里有一个细节分类和商品是分开两个接口拉的而不是一次性把全店商品拉到前端再根据分类筛选。原因很简单——如果商品数量多一次性拉全量数据会让首屏变慢更重要的是分类筛选交给后端商品上下架的状态变更能即时生效。前端过滤虽然省一次请求但会出现管理端下架了商品用户端还在显示的尴尬情况答辩演示时一旦被发现就很被动。商品列表的渲染不需要做分页。奶茶店 SKU 一般就是几十个到一两百个分页反而增加演示时翻页的复杂度。后端接口直接返回全量列表即可。3.2 购物车本地缓存先算价提交订单时后端重新对账购物车是这个项目里最能体现工程经验的部分。常见的错误是把购物车数据直接同步到后端每次加购都发一次请求。这样做既慢又容易在弱网环境下丢失操作。我的做法是购物车只存在本地 Storage下单时才把购物车明细传给后端后端重新从数据库查询商品价格并计算总价。加购逻辑// 通用购物车方法供菜单页、商品详情页复用 addToCart(e) { const item e.currentTarget.dataset.item const cart wx.getStorageSync(cart) || [] const idx cart.findIndex(p p.id item.id) if (idx -1) { cart[idx].count 1 // 已存在则数量 1 } else { // 注意只存 id、name、price、image不要存整个商品对象 cart.push({ id: item.id, name: item.name, price: item.price, image: item.image, count: 1 }) } wx.setStorageSync(cart, cart) wx.showToast({ title: 已加入, icon: success }) }购物车页面的计算很简单reduce一遍得出总价界面上的数量加减更新 Storage 后重新 setData。真正的关键在提交订单这一步// pages/cart/cart.js 提交订单 submitOrder() { const cart wx.getStorageSync(cart) if (!cart.length) { wx.showToast({ title: 购物车为空, icon: none }) return } const clientTotal cart.reduce((sum, p) sum p.price * p.count, 0) request(/api/order/create, POST, { items: cart.map(p ({ productId: p.id, count: p.count })), clientTotal: clientTotal, remark: this.data.remark }).then(res { wx.removeStorageSync(cart) wx.redirectTo({ url: /pages/order/detail/detail?id${res.data.orderId} }) }) }注意这里传给后端的是{ productId, count }不是商品名和单价。商品名和单价可能被篡改比如用户用抓包工具改价格后端必须根据 productId 重新从数据库取当前价格再算总价。本地算的clientTotal只用于后端比对校验如果两边对不上后端应该拒绝订单或者以后端计算为准。这个前端展示价、后端算实价的思路在答辩时讲出来是很加分的细节。另外一个值得提的点下单成功后要清空购物车但 wx.removeStorageSync 之前要注意 isSync 参数真机上某些基础库版本存在同步删除失败的兼容问题。保险做法是wx.setStorageSync(cart, [])把空数组写回去而不是直接 remove。3.3 下单与订单列表订单状态机怎么设计订单状态是这个项目的灵魂。我给奶茶店点餐项目设计的订单状态流转很简单五态足够0 待支付 → 1 已支付/制作中 → 2 已完成 ↘ 3 已取消待支付订单用户可以在订单详情页取消支付后进入制作中制作完成后由后台手动置为已完成已取消订单只做记录不参与统计。不要设计已退款这种状态毕设演示用不到把状态机搞复杂了反而容易在状态判断代码里出 bug。订单列表页的代码是所有页面里最基础的请求接口、遍历渲染、按状态显示不同样式。// pages/order/list/list.js Page({ data: { orders: [], statusMap: { 0: 待支付, 1: 制作中, 2: 已完成, 3: 已取消 } }, onShow() { // 每次页面显示都刷新保证支付/取消后列表是最新的 this.loadOrders() }, loadOrders() { request(/api/order/list?page1size20).then(res { this.setData({ orders: res.data.list }) }) }, cancelOrder(e) { const id e.currentTarget.dataset.id request(/api/order/cancel/${id}, POST).then(() { wx.showToast({ title: 已取消, icon: success }) this.loadOrders() }) } })订单列表刷新用onShow而不是onLoad这是一个很容易被忽略的点。onLoad 只在页面首次创建时执行一次从支付页返回列表页时不会触发用户会看到订单状态还是旧的。用 onShow 保证每次切到该页面都重新拉数据代价是多一次请求但奶茶店单量场景完全扛得住。后端订单接口的返回结构建议统一为分页格式{ code: 0, message: ok, data: { list: [ { id: 1001, orderNo: 20240601123400001, status: 1, totalAmount: 18.5, items: [ { productName: 珍珠奶茶, price: 12.0, count: 1 }, { productName: 椰果, price: 6.5, count: 1 } ], createTime: 2024-06-01 12:34:00 } ], total: 3, page: 1, size: 20 } }这个结构里订单列表直接带上 items 明细前端渲染就不用为每个订单再发一次详情请求。毕设场景数据量小宁可多传数据也要少做请求往返演示时才流畅。4. 后台系统与数据库核心表结构、管理接口与登录鉴权4.1 数据库设计用户、商品、订单三张核心表先定死数据库设计是这个项目最该花时间的地方因为答辩必问。我见过很多翻车案例问题都出在订单表设计上——有人把商品信息直接塞进订单表的一个文本字段查历史订单时解析字符串这是典型的把关系型数据库用成了文件系统。正确的做法是四张核心表加两张辅助表user用户、category商品分类、product商品、orders订单主表、order_item订单明细、还有一张 admin后台管理员。直接看建表语句-- 用户表 CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信唯一标识, nickname VARCHAR(50) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL COMMENT 选填取餐联系用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 商品分类表 CREATE TABLE category ( id INT NOT NULL AUTO_INCREMENT, name VARCHAR(20) NOT NULL COMMENT 分类名如鲜奶茶, sort INT DEFAULT 0 COMMENT 排序值小的在前, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类表; -- 商品表 CREATE TABLE product ( id INT NOT NULL AUTO_INCREMENT, category_id INT NOT NULL COMMENT 所属分类, name VARCHAR(50) NOT NULL COMMENT 商品名, price DECIMAL(6,2) NOT NULL COMMENT 单价单位元, image VARCHAR(255) DEFAULT NULL COMMENT 图片URL, stock INT NOT NULL DEFAULT 0 COMMENT 库存0表示卖完, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, sales INT NOT NULL DEFAULT 0 COMMENT 累计销量列表页展示用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; -- 订单主表 CREATE TABLE orders ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号用户可见, user_id INT NOT NULL COMMENT 下单用户, total_amount DECIMAL(8,2) NOT NULL COMMENT 订单总金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1制作中 2已完成 3已取消, remark VARCHAR(255) DEFAULT NULL COMMENT 用户备注如去冰少糖, pay_time DATETIME DEFAULT NULL, finish_time DATETIME DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; -- 订单明细表 CREATE TABLE order_item ( id INT NOT NULL AUTO_INCREMENT, order_id INT NOT NULL COMMENT 对应订单主表id, product_id INT NOT NULL COMMENT 商品id, product_name VARCHAR(50) NOT NULL COMMENT 商品名快照, price DECIMAL(6,2) NOT NULL COMMENT 成交单价快照, count INT NOT NULL COMMENT 购买数量, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;这段 DDL 里最值得讲的是 order_item 表的设计。它冗余了 product_name 和 price 两个字段这叫快照。为什么必须冗余因为商品可以改价、可以下架甚至删除但历史订单的成交价和商品名必须保持不变。如果不快照订单详情页关联查询商品表商品改了名用户看到的订单就变样了。这个设计从电商系统里来答辩时主动说出订单明细做了商品快照避免历史订单受商品变更影响这句话就已经把数据库设计的分拿到手了。订单号 order_no 不要用自增 id 直接展示给用户。常见做法是用时间戳加随机数生成比如yyyyMMddHHmmss 4位随机数保证唯一即可。数据库层面加上唯一索引兜底防止并发下随机数撞车。4.2 管理端接口商品管理、订单状态流转的后台实现后台系统在高分毕设里是加分项但不需要做得多华丽。一套商品管理增删改查、上下架加订单管理查看、改状态就够了。管理端页面建议用 Vue Element UI 套一个后台模板接口用 Spring Boot 写。这里给出两个核心接口的写法。商品上下架接口RestController RequestMapping(/api/admin/product) public class AdminProductController { Resource private ProductMapper productMapper; /** 修改商品上下架状态 */ PostMapping(/status) public Result updateStatus(RequestParam Integer id, RequestParam Integer status) { if (status ! 0 status ! 1) { return Result.error(状态参数不合法); } int rows productMapper.updateStatus(id, status); if (rows 0) { return Result.error(商品不存在); } return Result.success(); } }订单状态流转接口RestController RequestMapping(/api/admin/order) public class AdminOrderController { Resource private OrderMapper orderMapper; /** 管理员更新订单状态制作中 - 已完成 */ PostMapping(/status) public Result updateStatus(RequestParam Long orderId, RequestParam Integer status) { Order order orderMapper.selectById(orderId); if (order null) { return Result.error(订单不存在); } // 简单的状态机校验只允许从制作中流转到已完成 if (order.getStatus() 1 status 2) { orderMapper.updateStatus(orderId, status); return Result.success(); } return Result.error(非法状态流转); } }这里有两个很容易被忽略的细节。第一管理端接口和用户端接口必须分开路径前缀/api/admin/并且管理员接口要有单独鉴权不能和用户 token 混用。我习惯的做法是管理端登录后发一个独立的 adminToken拦截器里对 /api/admin/** 做单独校验。第二订单状态流转要做状态机校验——不是任何状态都能跳到任何状态后端的 if 判断必须在 Service 层而不是 Controller 层这样多个接口复用时有统一校验。管理端的商品编辑页还有一个坑图片上传。管理端上传的商品图要存到服务器本地或对象存储数据库只存相对路径。如果存在服务器本地要注意给上传目录单独配置静态资源映射否则图片 URL 访问返回 404。4.3 微信登录与鉴权code 换 openid别再自己存密码很多第一次做微信小程序的同学会惯性思维做一个用户名密码注册登录。这在奶茶店点餐场景里完全是多余的。微信小程序的用户身份验证链路是固定的小程序端 wx.login 拿到临时 code后端拿 code 调用微信接口换取 openid再用 openid 查用户表查到就返回 token查不到就自动注册。小程序端代码// pages/user/user.js wx.login({ success: (res) { if (!res.code) { wx.showToast({ title: 登录失败, icon: none }) return } request(/api/auth/login, POST, { code: res.code }).then(res { wx.setStorageSync(token, res.data.token) wx.setStorageSync(userInfo, res.data.userInfo) }) } })后端处理逻辑PostMapping(/api/auth/login) public Result login(RequestBody MapString, String body) { String code body.get(code); // 调用微信接口 code 换 openid String openid wechatService.code2Session(code); if (openid null) { return Result.error(微信登录失败); } User user userMapper.selectByOpenid(openid); if (user null) { // 首次进入自动注册 user new User(); user.setOpenid(openid); userMapper.insert(user); } // 生成 token有效期设 7 天 String token JwtUtil.createToken(user.getId(), 7 * 24 * 3600); return Result.success(Map.of(token, token, userInfo, user)); }openid 是微信用户在当前小程序下的唯一标识同一个用户在不同小程序里 openid 不同但同一小程序内永远不变所以它是 user 表的天然唯一键。注意 code 只能用一次5 分钟内有效不能缓存也不能重复使用。后端拿到 openid 后不要直接把它当 token 返给前端因为 openid 泄露出去等于泄露了用户身份。正确做法是生成一个 JWTpayload 里只放 userId不放 openid。鉴权拦截器要做的事很简单解析 Authorization 头里的 token取出 userId放到请求上下文里。后面创建订单、查订单列表的接口都从上下文里取 userId而不是信任前端传过来的参数。这一步不做的话任何人都可以通过改请求参数查到别人的订单属于比较低级但扣分很重的漏洞。5. 项目避坑指南支付、域名、审核与并发的 5 个高频问题5.1 个人主体开通不了微信支付用模拟支付挡住这个坑现象开发到下单环节按照微信支付官方文档接入却发现个人主体的小程序账号根本没有申请支付能力的入口。申请商户号时被要求提供营业执照个人开发者只能干瞪眼。原因微信支付商户号要求主体必须是企业或个体工商户个人开发者无法开通。这是平台规则不存在绕过方案。解决毕设项目不要硬接真实支付接口位留好用一个模拟支付按钮代替。用户点去支付时前端弹窗提示演示模式点击确认即模拟支付成功然后调用后端一个标记支付接口PostMapping(/api/order/pay) public Result pay(RequestParam Long orderId) { // 真实项目这里会调用微信支付统一下单接口 // 毕设演示直接把订单状态从待支付改为制作中 orderMapper.updateStatus(orderId, 1); return Result.success(); }答辩时主动说因为个人主体没有微信支付商户号资质所以这里用模拟支付替代真实上线时替换为微信支付接口即可。这句话说完老师不仅不会扣分反而会觉得你对上线链路是清楚的。如果一定要演示真实支付需要去借一个有营业执照的个体户主体来申请商户号但毕设阶段不建议为了演示去走资质流程时间成本太高。5.2 开发者工具里正常、真机一开就白屏域名校验和 HTTPS 没配现象在微信开发者工具里所有接口请求正常菜单、订单都能加载。一发到手机预览页面能打开但所有数据都是空的控制台里全是 request:fail 或 url not in domain list。原因开发者工具默认勾选了不校验合法域名所以 HTTP 接口在工具里能跑。但真机上 wx.request 强制要求域名必须是 HTTPS、必须在微信公众平台后台的服务器域名里配置过。两个条件缺一个都会失败。解决分场景处理。本地联调阶段真机预览需要打开开发版的调试模式——在开发者工具点预览生成二维码手机上打开时点右上角··· → 开发调试 → 打开调试这样真机也能跳过域名校验。部署阶段把接口放到一个已备案域名下配置 HTTPS 证书然后在公众平台「开发管理 → 开发设置 → 服务器域名」里把 request 合法域名加上。这里有个很多新手不知道的细节配置的域名不能带路径只能写到根域名而且域名下的所有子路径都会生效。另外 localhost 在真机上永远不可用必须用你电脑的局域网 IP而且此时手机和电脑要在同一网络下。如果只想快速给答辩老师演示建议直接用预览版的调试模式不要折腾域名。5.3 小程序审核被拒点餐类目需要资质毕设别硬碰现象想把小程序正式发布上线提交代码审核后被驳回理由是你的小程序涉及餐饮点单服务但未提交相应资质。原因微信对涉及食品、餐饮的类目有严格准入需要《食品经营许可证》之类的资质文件。个人主体小程序开放的服务类目非常有限餐饮点餐不在范围内。解决毕设场景下根本不走正式发布流程。项目做完后在小程序后台把代码上传为体验版生成体验版二维码答辩老师扫码即可体验完整功能。体验版不受类目审核限制这是毕设演示的标准姿势。如果老师要求看上线效果就提前把体验版二维码打出来贴在答辩 PPT 里。千万不要为了上线去挂靠资质或借用他人资质出了合规问题得不偿失。5.4 管理端上传的商品图在真机上裂图路径大小写和域名拼接现象在本地开发时管理端上传的图片显示正常部署到云服务器后小程序端商品图一片空白。用浏览器直接打开图片 URL 却能访问但在小程序里就是裂图。原因两个常见原因叠在一起。第一本地开发用 Windows文件系统不区分大小写但 Linux 服务器严格区分。上传时图片扩展名是 .JPG代码拼接路径时写 .jpgWindows 上能访问Linux 上 404。第二数据库里存的是相对路径 /upload/xxx.jpg接口返回时没有拼上图片服务器的域名前缀小程序端拿到的 URL 无法直接访问。解决上传时统一把文件名转成小写扩展名也强制小写存储相对路径返回给前端时统一拼接。在管理端上传接口里加一行// 文件名统一转小写避免 Linux 大小写问题 String ext originalFilename.substring(originalFilename.lastIndexOf(.) 1).toLowerCase(); String fileName System.currentTimeMillis() _ new Random().nextInt(1000) . ext;前端拿到接口返回的图片路径后用IMG_BASE path方式拼接展示。路径拼接的函数在 request.js 同级建一个 utils/format.js 统一处理不要在页面里到处拼。5.5 并发下单库存超卖先扣库存再校验行数现象促销场景下两个用户同时下单买同一款限量奶茶系统显示库存还剩 1 杯但两个订单都创建成功了。原因后端逻辑写成了先 select 查库存 → 判断是否大于 0 → 再 update 扣库存。这两个操作之间有间隙并发时两个请求同时读到库存为 1同时通过判断又同时执行 update——扣成了 -1。这就是典型的超卖。解决把判断和扣减合并成一条原子 SQL数据库行锁保证同一时间只有一个请求能成功扣减UPDATE product SET stock stock - 1 WHERE id ? AND stock 1受影响行数为 1 说明扣减成功为 0 说明库存不足。在 Service 层这样写int rows productMapper.deductStock(productId, count); if (rows 0) { throw new BizException(库存不足); }这里 count 也要传进去SQL 变成SET stock stock - #{count} WHERE id #{id} AND stock #{count}。如果后续要做更复杂的方案可以引入库存流水表记录每次扣减但毕设能做到这一步已经足够说明你对并发问题的认识。6. 从能演示到高分答辩验收清单、加分扩展与上线检查6.1 答辩前按这份清单自测一遍我见过太多项目在演示到一半时当场翻车。不是功能没做而是没有按完整流程自测。答辩前至少完整走一遍这条链路检查项通过标准登录链路小程序冷启动 → 自动登录 → 用户页显示微信头像昵称点餐链路切换分类 → 加购 → 购物车算价 → 提交订单 → 订单列表出现待支付单后台链路管理端登录 → 看到新订单 → 修改状态为制作中 → 小程序端订单状态同步变化异常路径空购物车提交有提示、库存为 0 的商品不可加购、未登录访问订单页有拦截数据初始化管理端至少预置 3 个分类、8 个商品价格不要都是整数要有 .5 这种小数前三条是主流程必须通后两条是刹车片很多项目栽在这上面。比如库存为 0 的商品前端按钮置灰了但接口层是否也做了校验如果只做了前端限制演示时被人截包绕过前端还是能下单成功。6.2 三个低成本加分扩展如果主流程已经稳定时间还够优先做这三个扩展它们都是在小程序端小改、后端加一张表或一个统计接口就能完成的。第一个是优惠券。用户中心加一个领券入口后端建 coupon 用户券表订单创建时校验用户是否有可用券在算总价时减去优惠金额。订单表加一个 discount_amount 字段注意对账时总额 商品原价合计 - 优惠金额。第二个是订单超时自动取消。待支付订单 15 分钟未支付由后端定时任务扫描并取消。Spring Boot 里用 Scheduled 注解每分钟扫一次待支付订单把创建时间超过 15 分钟的订单置为已取消。这个功能不需要引入消息队列一个定时任务足以撑起毕设演示。第三个是销量统计。管理端加一个简单的报表页统计每日订单数和销售额用 ECharts 画柱状图。SQL 就是按日期 group by 一下整个功能半天能做完但答辩展示效果非常直观老师一看就知道你有数据化运营的思维。6.3 上线与演示的取舍最后说一个我觉得很重要的习惯演示之前永远准备一套保底数据。意思是所有接口的返回都要保证在断网或者后端挂了的情况下前端页面不会白屏——购物车本地有缓存、菜单页有历史数据兜底。我曾经在答辩现场遇到云服务器被同组同学误删配置导致后端起不来幸好首页菜单数据做了本地缓存兜底演示才没有当场中断。这个习惯后来帮我避过好几次类似的坑。奶茶店点餐这个题目本身不难难的是把链路做完整、把边界想清楚。你愿意把支付、并发、类目这些真实上线才会遇到的问题想明白已经比大多数停留在页面能跑的毕设高出一截了。希望这篇文章能帮你少走一段弯路顺利把项目做成能拿高分的样子。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询