ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue+微信小程序奶茶店点餐系统前后端分离实战

SpringBoot+Vue+微信小程序奶茶店点餐系统前后端分离实战 简介这是一套基于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 为什么是SpringBootVue小程序这套组合做奶茶店点餐这类业务系统核心诉求是用户端点单、商家端管理、数据集中存。微信小程序天然适合做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接口。所以接口的返回格式必须统一不然前端每个页面都得单独处理异常。我一般定义一个ResultT通用返回体包含三个字段code200成功、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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 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注解。serverTimezoneAsia/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 ResultString wxLogin(RequestBody MapString, String params) { String code params.get(code); // 调用微信code2Session接口实际项目用RestTemplate或OkHttp String openid wechatService.code2Session(code); User user userMapper.selectOne( new LambdaQueryWrapperUser().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 ResultListCategoryVO menu() { ListCategory categories categoryMapper.selectList( new LambdaQueryWrapperCategory().orderByAsc(Category::getSort)); ListCategoryVO result categories.stream().map(c - { CategoryVO vo new CategoryVO(); BeanUtils.copyProperties(c, vo); ListProduct products productMapper.selectList( new LambdaQueryWrapperProduct() .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 ResultString addCart(RequestAttribute(userId) Long userId, RequestBody CartItemDTO dto) { Cart cart cartMapper.selectOne( new LambdaQueryWrapperCart() .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(existfalse)加关联查询逻辑直白数据量小的时候性能没问题。真到了商品几千条的场景再考虑单独写联表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. 查出购物车中选中的商品 ListCart carts cartMapper.selectList( new LambdaQueryWrapperCart() .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()); ListOrderItem 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 LambdaQueryWrapperCart() .eq(Cart::getUserId, userId) .eq(Cart::getChecked, 1)); return orderVO; }Transactional(rollbackFor Exception.class)这个注解默认只回滚运行时异常rollbackFor明确指定所有异常都回滚这是血泪经验——不加这个参数业务里抛出的自定义异常不会触发事务回滚会出现订单创建了但库存没扣的脏数据。deductStock是自定义SQL我写在ProductMapper里update iddeductStock 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 :dataorderList border stripe el-table-column proporderNo label订单号 width200 / el-table-column proptotalAmount label金额 width100 / el-table-column label状态 width140 template slot-scopescope el-tag :typestatusMap[scope.row.status].type {{ statusMap[scope.row.status].label }} /el-tag /template /el-table-column el-table-column label操作 width180 template slot-scopescope el-button sizemini typeprimary clickhandleStatus(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() }) } } } /scriptstatusMap这个对象把数字状态映射成中文描述和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里配置Jacksonspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8同时检查实体类时间字段有没有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接口设计和小程序生态里积累出一套属于自己的可复用经验。本文还有配套的精品资源点击获取
返回列表