简介:这是一套基于SpringBoot与Vue的前后端分离奶茶店点餐微信小程序完整源码包,附带数据库脚本,适合作为毕业设计、期末大作业或课程设计项目,也方便新手通过注释与文档快速上手。压缩包内含481个文件,共5.63MB,涵盖101个Java后端类、50个Vue前端组件、57个JavaScript与相关小程序脚本、SQL数据库脚本、106个PNG及104个JPG等图片资源,以及yml、md、json等配置与说明文档,目录结构清晰,便于按模块查找。该项目在CSDN已有368人学习浏览。源码包含详细代码注释和使用文档,业务覆盖点餐、订单、管理等核心环节,界面美观、功能完整,经过调试可直接部署运行,能够帮助学习者节省搭建时间,重点理解前后端分离与小程序交互的实际应用。
1. 拿到"奶茶店点餐微信小程序"题目之后:先看清这四层代码
很多人在毕设题目里看到"基于SpringBoot和Vue的前后端分离奶茶店点餐微信小程序源码+数据库",第一反应是代码多、学不完。其实拆开看就是四层:小程序给顾客点餐,Vue管理后台管菜单和订单,SpringBoot提供接口,MySQL存业务数据。这套前后端分离项目实战的组合覆盖了后端框架、前端框架、移动端和数据库,也是答辩时最容易被追问的架构。这篇按"数据库→后端→管理端→小程序"的顺序,把能直接复现的最小命令、核心代码和踩坑点写出来。新手照着做能跑通,熟手可以直接拿去做扩展。
2. 把数据库和接口契约先定死:六张表加三个约定
2.1 为什么是SpringBoot+Vue+小程序这套组合
做奶茶店点餐这类业务系统,核心诉求是"用户端点单、商家端管理、数据集中存"。微信小程序天然适合做C端点餐入口,不需要用户装App,扫码即用,这和奶茶店线下场景匹配。Vue做管理后台是因为它生态成熟,Element UI组件库拿来就能拼出订单列表和商品表单,开发效率比jsp那套老方案高一个量级。SpringBoot负责把业务逻辑做成REST接口,MyBatis-Plus做数据库操作,事务和拦截器都好配。
这套组合的另一个优势是学习路径顺。SpringBoot和Vue在招聘需求里出现频率极高,做完这个项目,后端接口设计、跨域处理、JWT鉴权、小程序登录这些技能点都能覆盖到。相比"若依框架前后端分离"那种重型脚手架,自己从空项目搭一遍,对框架的理解会扎实很多,答辩时老师问底层机制也不慌。
2.2 六张表的关系与建表SQL:用户、分类、商品、购物车、订单、订单明细
数据库设计是这套代码的地基。奶茶店点餐的业务模型不复杂,但表之间关系要理清。用户表存小程序端授权后的用户信息;分类表存"招牌奶茶""鲜果茶""小料"这种分组;商品表挂在分类下;购物车表按用户维度存临时选择;用户下单后生成订单表和订单明细表,订单明细冗余商品名称和图片,防止商品改价或删除后历史订单查不到痕迹。
我给的建表SQL按常见毕业设计标准来,字段命名统一用下划线,MyBatis-Plus开启驼峰映射后可以直接对应实体类。
CREATE DATABASE IF NOT EXISTS milk_tea DEFAULT CHARACTER SET utf8mb4; USE milk_tea; CREATE TABLE user ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '主键', openid VARCHAR(64) UNIQUE 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 COMMENT '注册时间' ) COMMENT '用户表'; CREATE TABLE category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT '分类名', sort INT DEFAULT 0 COMMENT '排序值,越小越靠前' ) COMMENT '商品分类表'; CREATE TABLE product ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT '所属分类', name VARCHAR(64) NOT NULL COMMENT '商品名', price DECIMAL(10,2) NOT NULL COMMENT '售价', image VARCHAR(255) DEFAULT '' COMMENT '商品图片', description VARCHAR(255) DEFAULT '' COMMENT '描述', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', stock INT DEFAULT 0 COMMENT '库存', sales INT DEFAULT 0 COMMENT '销量', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) COMMENT '商品表'; CREATE TABLE cart ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '用户ID', product_id BIGINT NOT NULL COMMENT '商品ID', quantity INT DEFAULT 1 COMMENT '数量', checked TINYINT DEFAULT 1 COMMENT '1选中 0未选中', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_product (user_id, product_id) ) COMMENT '购物车表'; CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL COMMENT '订单号', user_id BIGINT NOT NULL COMMENT '用户ID', total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总金额', status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2制作中 3待取餐 4已完成 5已取消', remark VARCHAR(255) DEFAULT '' COMMENT '备注', address VARCHAR(255) DEFAULT '' COMMENT '取餐人/地址', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME DEFAULT NULL COMMENT '支付时间' ) COMMENT '订单表'; CREATE TABLE order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT '订单ID', product_id BIGINT NOT NULL COMMENT '商品ID', product_name VARCHAR(64) NOT NULL COMMENT '商品名称快照', product_image VARCHAR(255) DEFAULT '' COMMENT '商品图片快照', price DECIMAL(10,2) NOT NULL COMMENT '成交单价', quantity INT NOT NULL COMMENT '购买数量' ) COMMENT '订单明细表';MySQL 8.0 以上版本跑这段SQL没有问题,如果是 5.7 需要把 utf8mb4 的排序规则显式写成 utf8mb4_general_ci 也行。注意orders表名用了复数,因为order是 MySQL 的保留关键字,直接用会报语法错误,这是新手最容易翻车的地方。
建表时的几个设计取舍值得说:购物车表对(user_id, product_id)建了联合唯一索引,这样同一商品加入购物车时可以直接走ON DUPLICATE KEY UPDATE做数量累加,不用先查再改。订单明细冗余商品快照字段,是为了保证商品下架后历史订单仍能正确展示。库存字段放在了 product 表里,下单时用UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity}这种乐观方式扣减,避免超卖。
2.3 接口返回格式与JWT:前后端分离的"契约"
前后端分离的项目,前后端各跑各的端口,交流全靠HTTP接口。所以接口的返回格式必须统一,不然前端每个页面都得单独处理异常。我一般定义一个Result<T>通用返回体,包含三个字段:code(200成功、4xx业务错误、500异常)、msg(提示信息)、data(载荷数据)。所有Controller都返回这个结构,前端axios拦截器里统一判断code,等于200就进成功回调,否则弹出msg。这样下单失败、库存不足、未登录这些业务错误不用每个页面单独写。
登录状态用JWT解决。小程序端用户首次进入时调用wx.login拿到临时 code,后端拿着 code 调微信接口换 openid,然后用 JWT 生成 token 返回给小程序。小程序后续每次请求在 header 里带Authorization: Bearer token。后端用一个拦截器解析 token,把用户ID塞进请求上下文。管理端(Vue)用的是账号密码登录,同样发JWT,只是载荷里多了角色字段。
JWT的好处是服务端不用存session,小程序端也没有cookie机制,天然适配这种无状态架构。要注意token有效期一般设7天,小程序端每次启动时如果发现请求返回401,就静默调一次登录接口刷新token。这个逻辑放在request封装里统一处理,不用每个页面都管。
3. SpringBoot后端从零搭起:登录、购物车、下单的完整链路
3.1 项目初始化与核心依赖:Maven配置里这几项别漏
后端工程我建议直接用 Spring Initializr 生成,Java版本选8或11都行,SpringBoot版本选2.7.x。为什么不用SpringBoot 3?因为3.0强制要求JDK17,很多学校的服务器环境还是JDK8,而且微信小程序相关的旧教程里依赖版本对不上,踩坑成本高。2.7.x稳定、教程多、MyBatis-Plus兼容性好,做毕业设计足够。
pom.xml 里核心依赖如下:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>这里mybatis-plus版本用3.5.3.1而不是更高版本,是因为新版分页插件配置方式有变化,教程里大部分代码基于3.5.x早期版本写的,直接照抄容易跑不通。jjwt 0.9.1这个版本用的是io.jsonwebtoken.Jwts.builder()的经典写法,网上教程最多的也是它。
application.yml里核心配置如下:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/milk_tea?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case开启后,create_time这个数据库字段会自动映射到Java实体类的createTime属性,不用写一堆@TableField注解。serverTimezone=Asia/Shanghai必须加,不然MySQL 8的时区问题会导致查询报错。StdOutImpl会在控制台打印SQL,调试时能看到MyBatis-Plus实际执行的语句,排查数据问题很有用。
3.2 JWT登录与拦截器:小程序端怎么被识别
小程序端登录走的是微信的code2Session接口。后端收到前端传来的 code 后,调用微信接口拿到 openid,查 user 表,没有就自动注册一个,然后签发 JWT。
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private UserMapper userMapper; @PostMapping("/wx-login") public Result<String> wxLogin(@RequestBody Map<String, String> params) { String code = params.get("code"); // 调用微信code2Session接口,实际项目用RestTemplate或OkHttp String openid = wechatService.code2Session(code); User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getOpenid, openid)); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname("微信用户" + openid.substring(0, 6)); userMapper.insert(user); } String token = JwtUtil.createToken(user.getId(), "user"); return Result.success(token); } }注意LambdaQueryWrapper是MyBatis-Plus推荐的条件构造器,用方法引用代替字符串列名,编译期就能发现字段名写错的问题。JwtUtil.createToken内部做的事就是把用户ID和角色塞进JWT的claim,再设置过期时间签名。token 有效期建议7天,太短会导致用户频繁重新登录,太长有被盗用的风险。
拦截器配置是另一个关键点。需要拦截所有/api/**请求,但放行/api/auth/**和/api/product/**的查询接口,因为小程序首页要展示商品列表,用户没登录也能看。我用一个HandlerInterceptor实现,在preHandle里解析token,把用户ID放到request.setAttribute("userId", ...),后面的Controller直接用@RequestAttribute("userId") Long userId取。
3.3 商品列表与购物车接口:点餐流程的后端逻辑
商品列表接口是点餐流程的入口,小程序首页需要按分类展示商品。一个接口返回分类和对应商品,比前端分别调两次接口更高效,也符合奶茶店菜单"左边分类、右边商品"的交互形态。
@GetMapping("/menu") public Result<List<CategoryVO>> menu() { List<Category> categories = categoryMapper.selectList( new LambdaQueryWrapper<Category>().orderByAsc(Category::getSort)); List<CategoryVO> result = categories.stream().map(c -> { CategoryVO vo = new CategoryVO(); BeanUtils.copyProperties(c, vo); List<Product> products = productMapper.selectList( new LambdaQueryWrapper<Product>() .eq(Product::getCategoryId, c.getId()) .eq(Product::getStatus, 1)); vo.setProducts(products); return vo; }).collect(Collectors.toList()); return Result.success(result); } @PostMapping("/cart/add") public Result<String> addCart(@RequestAttribute("userId") Long userId, @RequestBody CartItemDTO dto) { Cart cart = cartMapper.selectOne( new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getProductId, dto.getProductId())); if (cart == null) { cart = new Cart(); cart.setUserId(userId); cart.setProductId(dto.getProductId()); cart.setQuantity(dto.getQuantity()); cartMapper.insert(cart); } else { cart.setQuantity(cart.getQuantity() + dto.getQuantity()); cartMapper.updateById(cart); } return Result.success("加入成功"); }menu接口里用BeanUtils.copyProperties把 Category 的属性拷到 CategoryVO,是因为VO里多了一个products列表字段,直接返回实体类也行,但严格分层更规范。这里用两次查询而不是@TableField(exist=false)加关联查询,逻辑直白,数据量小的时候性能没问题。真到了商品几千条的场景,再考虑单独写联表SQL。
购物车添加接口利用联合唯一索引,先查再决定插入还是更新。这个写法虽然多一条查询语句,但逻辑清晰,不容易出并发问题。还有一种更省SQL的写法是INSERT ... ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity),MyBatis-Plus里要写自定义SQL,对新手不太友好,我推荐先用简单版。
3.4 下单接口与订单状态流转:事务边界要划清楚
下单是整套系统里最容易出bug的地方。一个订单要同时做四件事:查询商品最新价格、扣减库存、创建订单记录、创建订单明细。这四步必须在一个事务里,任何一个失败都要回滚。
@Transactional(rollbackFor = Exception.class) @Override public OrderVO createOrder(Long userId, CreateOrderDTO dto) { // 1. 查出购物车中选中的商品 List<Cart> carts = cartMapper.selectList( new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getChecked, 1)); if (carts.isEmpty()) { throw new BusinessException("购物车为空"); } // 2. 生成订单号并计算总价 String orderNo = "MT" + System.currentTimeMillis() + String.format("%03d", new Random().nextInt(1000)); BigDecimal totalAmount = new BigDecimal("0.00"); // 3. 创建订单主表记录 Orders order = new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setStatus(0); order.setRemark(dto.getRemark()); List<OrderItem> items = new ArrayList<>(); for (Cart cart : carts) { Product product = productMapper.selectById(cart.getProductId()); if (product == null || product.getStatus() != 1) { throw new BusinessException("商品已下架"); } // 乐观锁扣库存:stock >= 0 才允许扣减 int updated = productMapper.deductStock(product.getId(), cart.getQuantity()); if (updated == 0) { throw new BusinessException("库存不足"); } totalAmount = totalAmount.add( product.getPrice().multiply(new BigDecimal(cart.getQuantity()))); OrderItem item = new OrderItem(); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setProductImage(product.getImage()); item.setPrice(product.getPrice()); item.setQuantity(cart.getQuantity()); items.add(item); } order.setTotalAmount(totalAmount); orderMapper.insert(order); // 4. 批量插入订单明细 for (OrderItem item : items) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } // 5. 清空已下单的购物车 cartMapper.delete( new LambdaQueryWrapper<Cart>() .eq(Cart::getUserId, userId) .eq(Cart::getChecked, 1)); return orderVO; }@Transactional(rollbackFor = Exception.class)这个注解默认只回滚运行时异常,rollbackFor明确指定所有异常都回滚,这是血泪经验——不加这个参数,业务里抛出的自定义异常不会触发事务回滚,会出现订单创建了但库存没扣的脏数据。
deductStock是自定义SQL,我写在ProductMapper里:
<update id="deductStock"> UPDATE product SET stock = stock - #{quantity}, sales = sales + #{quantity} WHERE id = #{id} AND stock >= #{quantity} </update>用WHERE stock >= #{quantity}做条件更新,数据库层面保证不会扣成负数。如果更新影响行数为0,说明库存不够,直接抛异常。这个方案比先查库存再扣减的写法可靠,天然防超卖,不需要分布式锁。
库存扣减成功、订单创建失败的情况也考虑到了:事务回滚后,库存也会回滚,不会出现只有订单没有库存的账目不一致。订单状态机从0到4依次是待支付、已支付、制作中、待取餐、已完成,管理端在订单列表里通过Vue的el-select切换状态,后端提供更新接口校验状态流转的合法性。
4. Vue管理端和微信小程序端:把接口用起来才算前后端分离
4.1 管理端项目结构与路由设计:Vue3还是Vue2要想清楚
管理端我建议用Vue2 + Element UI,不要一上来就追Vue3。原因很实际:Element UI的表格、弹窗、表单组件在这个场景开箱即用,Vue3对应的Element Plus文档和示例虽然也在完善,但很多老教程的代码迁移过来需要改语法。你拿到手的项目如果写的是Vue2,留在Vue2能省一多半调bug的时间。
项目的结构按功能模块划分:
src/ ├── api/ # 按模块封装的接口请求 │ ├── product.js │ ├── order.js │ └── login.js ├── router/ # 路由配置 ├── views/ │ ├── Login.vue │ ├── Dashboard.vue │ ├── product/ │ │ ├── List.vue │ │ └── Edit.vue │ └── order/ │ └── List.vue ├── utils/ │ └── request.js # axios封装 └── App.vue路由设计上用懒加载,按需加载路由对应的组件,首屏加载会快不少。路由守卫beforeEach里检查本地localStorage有没有token,没有就跳转登录页。菜单项和路由一一对应,路由跳转时配合vue-router的meta.title改浏览器标题。这些是Vue项目的基本功,做管理端页面时能自然用上。
4.2 商品管理和订单管理页面:el-table + axios封装的套路
订单管理页面是整个管理端的核心。店长要在后台看到新订单、确认制作、标记取餐状态。页面用el-table展示订单列表,通过el-select切换状态,核心逻辑如下:
// api/order.js import request from '@/utils/request' export function getOrderList(params) { return request({ url: '/api/order/list', method: 'get', params }) } export function updateOrderStatus(id, status) { return request({ url: `/api/order/${id}/status`, method: 'put', data: { status } }) }<!-- views/order/List.vue 核心部分 --> <template> <el-table :data="orderList" border stripe> <el-table-column prop="orderNo" label="订单号" width="200" /> <el-table-column prop="totalAmount" label="金额" width="100" /> <el-table-column label="状态" width="140"> <template slot-scope="scope"> <el-tag :type="statusMap[scope.row.status].type"> {{ statusMap[scope.row.status].label }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="180"> <template slot-scope="scope"> <el-button size="mini" type="primary" @click="handleStatus(scope.row)"> 更新状态 </el-button> </template> </el-table-column> </el-table> </template> <script> import { getOrderList, updateOrderStatus } from '@/api/order' export default { data() { return { orderList: [], statusMap: { 0: { label: '待支付', type: 'warning' }, 1: { label: '已支付', type: 'success' }, 2: { label: '制作中', type: 'primary' }, 3: { label: '待取餐', type: 'info' }, 4: { label: '已完成', type: 'success' } } } }, created() { this.fetchList() }, methods: { fetchList() { getOrderList({ page: 1, pageSize: 10 }).then(res => { this.orderList = res.data.records }) }, handleStatus(row) { const next = (row.status + 1) % 5 updateOrderStatus(row.id, next).then(() => { this.$message.success('状态已更新') this.fetchList() }) } } } </script>statusMap这个对象把数字状态映射成中文描述和el-tag的type,页面模板里直接通过statusMap[scope.row.status]取用,比在模板里写一堆v-if判断干净得多。更新状态按钮直接用(row.status + 1) % 5循环切换,演示够了,实际项目应该用下拉框明确选择目标状态。
request.js里axios封装的拦截器要处理两件事:请求拦截器把token加到header里;响应拦截器统一判断code字段,非200时用Element UI的Message.error弹出错误信息。这里有个细节:后端返回401时,应该跳转登录页并清理本地token,否则会话过期后所有请求都在报错,体验很差。
4.3 小程序端点餐流程:菜单、购物车、下单三步走
小程序端是顾客直接使用的界面,交互流程要尽量短:进店看到菜单、选商品加购物车、一键下单。页面就三个核心的:首页商品列表页、购物车页、订单确认页。商品列表页用 scroll-view 做分类和商品的左右联动,顶部是分类tab,下面是商品卡片,点击商品弹窗选数量加购物车。
// utils/request.js 小程序端的请求封装 const BASE_URL = 'http://localhost:8080' function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + wx.getStorageSync('token') }, success(res) { if (res.data.code === 200) { resolve(res.data.data) } else if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }) reject(new Error('未登录')) } else { wx.showToast({ title: res.data.msg, icon: 'none' }) reject(new Error(res.data.msg)) } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }) reject(err) } }) }) } module.exports = request小程序没有axios,用的是wx.request,我封装了一个Promise化的request函数统一管理。注意wx.request的success回调里,HTTP状态码200只代表请求送达后端,真正业务成功要看res.data.code是不是200。这个区分很多新手搞混,看到HTTP 200就以为成功了,结果数据没拿到。
购物车页面用wx.setStorageSync做本地缓存,加购物车时先写本地,点击下单时把本地购物车数据提交到后端。这个方案的好处是响应快、省流量,缺点是如果用户在多个设备上登录,购物车数据不同步。实际门店场景顾客基本用同一台手机,问题不大。毕设演示时注意把"未登录先看菜单、点单时再登录"的流程跑通。
4.4 小程序联调配置:请求地址、登录态、图片域名
联调时最常遇到的是请求失败问题。开发工具里,小程序默认要求后端接口必须是HTTPS域名,但本地开发时后端跑在http://localhost:8080,直接请求会被拦截。
解决方式是在微信开发者工具右上角"详情→本地设置"勾选"不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书"。这个选项只对开发工具生效,真机预览要同时勾选"不校验合法域名"。上线时必须换成备案过的HTTPS域名,这是微信的硬性规定。
登录态的处理逻辑:小程序启动时先调wx.login()获取code,再传给后端换token。token存到wx.setStorageSync,后续每个请求在header里带。我习惯在App.vue的onLaunch里做一次静默登录,不阻塞首页渲染。如果token过期,后端返回401,就重新调一遍登录流程。
图片显示问题也经常遇到。商品图片和用户头像如果是http://localhost地址,在小程序里image组件的src是不认识这个域名的。开发阶段同样靠"不校验合法域名"解决,上线前图片要上传到对象存储或云存储,用HTTPS访问。管理端的图片上传接口一般是把文件存到服务器本地目录,再返回可访问的URL,这里要多留个心眼:存储路径不能写在SpringBoot的src/main/resources下,否则打包成jar后路径会失效。
5. 前端联调避坑:五个让新手翻车的常见问题
5.1 小程序请求直接报"request:fail"或404
现象:小程序页面请求后端接口,控制台报request:fail或者statusCode: 404,但同一接口在后端Postman测试是通的。
原因:两个位置容易出问题。一是BASE_URL写成了IP地址但手机和电脑不在同一网段,模拟器能通真机不能通;二是SpringBoot请求路径带了context-path,比如server.servlet.context-path: /api,小程序端BASE_URL里又加了一层/api,路径翻倍。我用Vue管理端联调时也踩过,后端配了context-path,管理端axios的baseURL又写/api,结果所有请求都是404,排查了半天。
解决:先把小程序端的BASE_URL改成http://localhost:8080确保模拟器通;真机调试时改成电脑的局域网IP,关闭防火墙或放行8080端口。路径问题用后端控制台日志确认实际请求的URL,比前端瞎猜快得多。SpringBoot在application.yml里加了context-path就记住后端所有接口都带前缀,前端baseURL不能再重复拼接。
5.2 跨域配置写了还是报403
现象:Vue管理端访问后端接口,浏览器控制台报跨域错误,后端加了@CrossOrigin注解或者全局CorsFilter,但还是被拦。
原因:我见过最多的场景是自定义拦截器的顺序问题。SpringBoot处理请求时,拦截器执行在CORS过滤之后,但如果拦截器直接返回了401/403响应,响应头里没有加Access-Control-Allow-Origin,浏览器就会判定为跨域失败。另一个情况是使用了spring-security作为依赖,安全框架的过滤器链把CORS配置覆盖了。
解决:用全局CorsFilter而不是注解方式,并且注册顺序提前。具体做法是在配置类里声明CorsFilterBean,用UrlBasedCorsConfigurationSource设置允许的allowedOriginPatterns。如果项目中加了Spring Security,在SecurityConfig里调用http.cors()并单独配置CorsConfigurationSource。建议调试阶段把所有来源都放行,上线再收敛到具体域名。
5.3 LocalDateTime在前端显示成数组
现象:订单创建时间在小程序端和Vue页面上显示成[2024, 5, 20, 14, 30, 25]这样的数组,或者格式变成2024-05-20T14:30:25,看着很别扭。
原因:SpringBoot默认用Jackson序列化LocalDateTime,序列化结果取决于配置。如果没有全局配置日期格式,默认输出ISO标准格式带T;如果项目中引入了fastjson且没有排除JDK8时间类,可能出现数组格式。
解决:统一在application.yml里配置Jackson:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8同时检查实体类时间字段有没有@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解,两个都配上最稳妥。fastjson如果没用就别引,一个项目里两套JSON解析器容易打架。前端拿到字符串格式的时间后就可以直接展示,不再需要额外处理。
5.4 图片上传成功但页面加载不出来
现象:Vue管理端上传商品图片显示成功,数据表里也有图片路径,但商品列表页图片裂了,小程序端更是直接空白。
原因:图片存到了本地磁盘路径,比如D:/upload/xxx.jpg或resources/static/upload/xxx.jpg,返回给前端的URL是相对路径,浏览器能访问但小程序访问不到本地路径。还有一个坑是SpringBoot打成jar包后,写入resources目录的文件在重启后丢失,因为jar包内部是只读的。
解决:开发阶段把上传目录配置为独立的磁盘目录,比如D:/workspace/upload/,然后通过一个静态资源映射把/upload/**路径映射到这个目录。用WebMvcConfigurer的实现类来配置资源映射。生产环境把图片放到单独的图片服务器或OSS,不要依赖应用服务器存文件。
5.5 购物车并发下单导致库存为负
现象:压力测试或两个人同时购买同一商品时,库存变成了负数,订单却都创建成功了。
原因:扣库存代码写成了"先查库存,判断大于0,再更新库存"的逻辑。在高并发下,两个请求同时查到库存是1,都通过了判断,分别执行扣减,最终库存变为-1。这就是经典的并发问题,我调试时用两个浏览器同时下单复现过。
解决:用章节3.4里提到的乐观锁更新,SQL写成UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity},影响行数为0就抛异常。如果项目里有多个地方更新库存,比如取消订单要加回库存,也都要走同一个Mapper方法,保持逻辑一致。
6. 从"本地跑通"到"能上演示":验证清单和三个进阶方向
6.1 本地跑通的最小验证清单
项目拿到手先别急着看代码,按顺序验证整个链路是否通畅。MySQL里建库导表,确认六张表都有数据;启动SpringBoot看控制台无报错,8080端口能访问;Vue管理端npm install && npm run dev,能打开登录页并用默认账号登录;微信开发者工具导入小程序项目,AppID选测试号,勾选不校验域名,能看到商品列表。这个清单十分钟能跑完,任何一步失败先修再往下走。跑通了再去看代码,你会对每层的作用有直观感知。
6.2 三个值得做的进阶方向
原项目能满足基本点餐流程,但答辩时想拿高分,至少做一到两个亮点。第一个是接微信支付的真实支付流程,用微信支付V3接口替换掉"模拟支付"的假按钮,这个方向面试官感兴趣,但流程长、证书配置麻烦,建议预留一周时间。第二个是增加数据统计面板,在管理端用ECharts展示每日营业额、热销商品TOP5、订单趋势图,代码量不大但视觉效果好。第三个是把商品图片存储迁移到MinIO对象存储,既能解决5.4节图片丢失问题,又能体现你对分布式存储的了解。
6.3 答辩时的验证思路
演示时不要只是把页面点一遍,要有逻辑地讲:先展示系统架构图,说明三端如何通过REST接口通信;再演示小程序端从浏览商品到下单的全流程,切到数据库实时展示订单表和库存字段的变化;最后展示管理端处理订单后,小程序端状态同步更新。这样一条线走下来,老师看到的不只是代码,而是你对整个业务闭环的掌控力。
我在给项目做最后验证时养成了一个习惯:每次跑完一个流程,都去看一眼MySQL里的数据产生了哪些变化。下单后查orders、order_item、product三张表的关联数据,确认金额、库存、状态都对得上。这个习惯让我躲过了很多只在页面层看似成功、数据库里早已出错的坑,也希望帮到你。
回看整个前后端分离项目实战过程,真正让你成长的不是照着源码敲一遍,而是动手去改一个功能、修一个bug。从今天这个奶茶店点餐小程序做起,跑通第一版,你就能在这个方向上的Vue脚手架、SpringBoot接口设计和小程序生态里,积累出一套属于自己的可复用经验。
本文还有配套的精品资源,点击获取