ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue前后端分离餐厅点餐系统开发实践

SpringBoot+Vue前后端分离餐厅点餐系统开发实践 从客户扫码到后厨出单整个点餐闭环看着简单真正动手做起来细节多得吓人。SpringBoot加Vue这套前后端分离组合是当前做餐厅点餐管理系统最常见也最稳妥的技术栈不管是拿来当毕设、课程设计还是给小餐馆做一套真正能用的信息化工具都绕不开订单流转、库存扣减、桌台管理这些硬骨头。这篇就把我做这类项目从零到落地的完整思路和踩坑记录整理出来希望能帮正在纠结怎么下手的朋友省点时间。先说清楚这个系统到底做什么顾客到店扫桌台码在手机上浏览菜品、加购物车、下单支付后厨或者吧台的大屏实时收到新订单提醒按状态处理出餐管理员在后台维护菜品分类、上下架、价格库存查看营业报表。就这么一条主线牵涉到用户端H5、商家管理端Web、后端服务、数据库再加上支付和打印对接是一个典型的前后端分离业务系统。我见过太多人一上来就急着写代码结果做到一半发现购物车和库存对不上订单状态乱成一团。这篇文章会从业务建模、数据库设计、后端接口、前端页面、部署上线到问题排查完整过一遍每个关键决策我都会解释为什么这么做以及我在实际开发中踩过哪些坑。1. 项目速览这个点餐系统到底在解决什么问题1.1 需求场景与系统边界餐厅点餐系统听起来很宽泛但落到实际开发必须先圈定场景。我做这个项目时默认的业务模型是“到店扫码点餐”而不是外卖平台那种复杂配送逻辑后者要牵扯骑手、分账、平台对接工作量完全不是一个量级。核心角色有三类顾客扫码进入点餐H5浏览菜单加购下单支付查订单状态。商家员工后厨/吧台接收新订单更新制作进度完成出餐。管理员店长/老板菜品管理、分类管理、桌台管理、订单查询、营业统计。为什么选扫码点餐而不是服务员拿PAD点餐因为扫码模式能大幅减少服务员人力成本顾客手机就是点餐终端商家不需要购买一堆硬件部署成本最低。这在中小型餐厅尤其吃香也是这类毕设和实际项目最常见的需求形态。1.2 为什么是SpringBoot Vue这套组合技术选型不能拍脑袋我当初对比过几套方案纯JSP/Servlet老项目、SpringBoot Thymeleaf服务端渲染、SpringBoot Vue前后端分离。方案优点缺点适用场景JSP/Servlet结构简单部署直接前后端耦合维护痛苦老系统维护SpringBoot Thymeleaf开发快不用跨域页面交互弱动态刷新体验差简单后台SpringBoot Vue前后端解耦团队并行交互流畅跨域、鉴权、部署复杂度上升本项目首选SpringBoot负责提供RESTful API处理业务逻辑、数据持久化、权限校验Vue负责页面渲染和用户交互通过Axios请求后端接口。这套组合的优势在于前端可以做成H5供顾客扫码访问也可以做成管理后台供商家用同一套后端API可以服务两个前端复用率非常高。另外从学习角度看SpringBoot自动装配机制能帮你省掉大量XML配置Vue的响应式数据绑定让页面开发效率成倍提升。这两样都是当前企业级开发的主流技能做个完整项目练一遍对找工作写简历也是实打实的帮助。1.3 核心功能模块清单我梳理了一下一个能真正跑起来的点餐系统功能模块大致如下用户模块顾客微信授权或手机号登录、员工账号登录、JWT鉴权、角色权限区分。菜品与分类模块菜品分类树、菜品CRUD、上下架、图片上传、规格大份/小份、库存。桌台模块桌台编号、二维码生成、桌台状态。购物车模块加购、修改数量、清空、实时计算总价。订单模块下单、订单状态流转、订单明细、支付对接微信/支付宝/模拟支付。消息推送模块WebSocket实时推送新订单到后厨大屏。统计报表模块营业额、订单量、菜品销量排行等。这些模块不是一上来全做我建议按照“用户登录 - 菜品展示 - 加购 - 下单 - 订单管理 - 统计报表”这条主线迭代开发每个环节可独立验证最后再串起来。2. 数据库设计别急着建表先把业务模型捋清楚2.1 核心表结构解析数据库设计是这类项目的地基。我见过不少人把订单和订单明细混在一张表里或者购物车字段乱放后面写SQL统计简直想哭。这里我按自己验证过的方案给出核心表结构。用户表sys_user主键id、username、passwordBCrypt加密存储、phone、role枚举CUSTOMER/EMPLOYEE/ADMIN、avatar、create_time。菜品分类表categoryid、name、sort排序权重、status1上架0下架。菜品表dishid、category_id关联分类、name、图片、price存储为Decimal(10,2)、description、status、stock库存数量、sales销量冗余字段用于排行。桌台表table_infoid、table_number、qr_code二维码图片地址、status空闲/占用。订单主表ordersid、order_no订单编号全局唯一、table_id、user_id、total_amount、status、pay_type、pay_status、remark、create_time、pay_time。订单明细表order_detailid、order_id、dish_id、dish_name冗余快照、dish_image、price下单时价格快照、quantity、subtotal。为什么订单明细要冗余冗余菜品名称和价格快照因为菜品价格和名称未来可能修改而历史订单必须保留下单那一刻的真实数据。这是电商系统里非常基础的设计约定不做就是给自己埋坑。状态字段我建议直接用TINYINT存数字枚举比如订单状态0待支付、1已支付/待制作、2制作中、3已完成、4已取消。有人喜欢用字符串查询和展示没问题但存储效率和索引性能都不如数字。后端用枚举类统一管理状态值前端只接收数字然后映射文案。2.2 MyBatis-Plus还是JPA我的选择ORM框架这块SpringBoot生态里最常见的就是MyBatis-Plus和Spring Data JPA。点餐系统既有简单的CRUD也有复杂统计SQL我最终选了MyBatis-Plus。原因很直接MyBatis-Plus提供通用Mapper、分页插件、条件构造器QueryWrapper单表操作几乎不用写SQL复杂统计查询可以手写XML映射文件完全可控。JPA虽然封装程度更高但碰到复杂查询时JPQL或原生SQL的坑对新手不太友好而且JPA的懒加载和N1问题在联表查询多的时候需要额外用心处理。核心依赖这样引入dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency2.3 建表SQL示例这里给出最关键的四张表建表语句其余表结构类似。字段注释一定要写清楚宁多勿少后期维护全靠注释。CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单编号, table_id bigint(20) DEFAULT NULL COMMENT 桌台ID, user_id bigint(20) NOT NULL COMMENT 下单用户ID, total_amount decimal(10,2) NOT NULL COMMENT 订单总金额, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2制作中 3已完成 4已取消, pay_type tinyint(4) DEFAULT NULL COMMENT 支付方式 1微信 2支付宝 3模拟, pay_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付, remark varchar(255) DEFAULT NULL COMMENT 订单备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, pay_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_status (status), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; CREATE TABLE order_detail ( id bigint(20) NOT NULL AUTO_INCREMENT, order_id bigint(20) NOT NULL COMMENT 订单ID, dish_id bigint(20) NOT NULL COMMENT 菜品ID, dish_name varchar(100) NOT NULL COMMENT 菜品名称快照, dish_image varchar(255) DEFAULT NULL COMMENT 菜品图片快照, price decimal(10,2) NOT NULL COMMENT 单价快照, quantity int(11) NOT NULL COMMENT 数量, subtotal decimal(10,2) NOT NULL COMMENT 小计, PRIMARY KEY (id), KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;这里有个细节order_no一定要建唯一索引。我一开始没做唯一约束并发测试时出现过重复订单号排查了很久才发现是多线程环境下时间戳加随机数撞了后来改成“日期 随机数 用户ID后缀”并加唯一索引才彻底解决。3. 后端SpringBoot核心实现从登录鉴权到下单扣库存3.1 项目初始化与分层结构用IDEA新建SpringBoot项目时我建议直接选Spring InitializrJava版本选8或11都行SpringBoot版本别追求最新2.7.x就可以稳定且资料多。选依赖时勾上Web、MySQL驱动、MyBatis后续换成MyBatis-Plus、Validation、Lombok。如果你手里是旧的Gradle项目想迁移到Maven也不用慌核心是把build.gradle里的依赖翻译成pom.xmlSpringBoot的starter命名是统一的换过去基本无痛。网上关于“早期gradle构建项目迁移maven”的教程不少核心就是注意版本号对齐。分层这块我按标准的三层架构Controller - Service - Mapper实体类放entity包DTO和VO也分开放。很多人觉得包分太多麻烦但项目一旦过千行包结构混乱就是灾难。我的推荐结构com.example.restaurant ├── controller # 接口层只做参数接收和结果返回 ├── service # 业务逻辑层 ├── mapper # MyBatis-Plus数据访问层 ├── entity # 数据库实体 ├── dto # 请求参数对象 ├── vo # 响应视图对象 ├── config # 配置类跨域、WebSocket、Security等 ├── common # 通用工具、统一返回结果、异常处理 └── utils # JWT、二维码生成等工具类3.2 JWT登录鉴权设计前后端分离项目的鉴权方案最常用的是JWTJSON Web Token。为什么不用Session因为前端H5和后台管理是不同端口甚至不同域名部署Session的Cookie跨域处理很别扭而JWT是无状态的后端不存登录状态前端每次请求在Header里带Authorization: Bearer token后端解析验证即可。JWT工具类核心代码我用的是jjwt库Component public class JwtUtils { Value(${jwt.secret}) private String secret; Value(${jwt.expire}) private Long expire; // 过期时间单位秒 public String generateToken(Long userId, String role) { Date now new Date(); Date expireDate new Date(now.getTime() expire * 1000); return Jwts.builder() .setHeaderParam(typ, JWT) .setSubject(userId.toString()) .claim(role, role) .setIssuedAt(now) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secret.getBytes(StandardCharsets.UTF_8)) .compact(); } public Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret.getBytes(StandardCharsets.UTF_8)) .parseClaimsJws(token) .getBody(); } }这里有几个实操要点第一secret不能硬编码在代码里要放到application.yml配置文件中不同环境用不同值。第二HS256签名时如果secret太短会报错建议至少32位以上。第三JWT过期时间我设置为24小时但在管理端场景玩家会希望更安全所以后端还需要一个拦截器校验token并刷新过期策略或者引导前端重新登录。拦截器配置里需要排除登录接口、菜品展示接口等匿名访问路径否则顾客还没登录就什么都看不到了。3.3 下单流程购物车到订单的完整链路下单是这个系统最核心的业务流流程大概是顾客在前端把购物车数据提交到后端格式是[{dishId: 1, quantity: 2}, ...]。后端校验菜品是否存在、是否上架、库存是否足够。计算总金额注意金额计算用BigDecimal绝对不能用double或float否则会出现0.10.2不等于0.3的经典问题。创建订单主表记录状态为待支付。创建订单明细记录。扣减库存这里用乐观锁防止超卖。返回订单ID和订单编号给前端前端引导用户支付。扣库存这一步很多人直接写成UPDATE dish SET stock stock - #{quantity} WHERE id #{id}这样在并发场景下会超卖。我用的方案是在库存表加一个乐观锁版本号字段或者直接在更新SQL里限定stock quantityUPDATE dish SET stock stock - #{quantity} WHERE id #{id} AND stock #{quantity}影响行数为0说明库存不足直接抛异常回滚事务。这种写法比先查再更新更安全也是SQL层面最简单的防超卖手段。下单接口我在Service层加了Transactional注解保证订单主表、明细表、库存扣减这三个操作要么全部成功要么全部回滚。这里要特别提醒事务只对RuntimeException回滚如果你自己try-catch吞掉了异常事务是不会回滚的。我刚开始做的时候就犯过这个错库存扣了但订单没生成排查半天才发现异常被吞了。3.4 WebSocket实时推送后厨大屏如何第一时间知道新订单点餐系统有个特别提升体验的功能后厨大屏实时显示新订单。一开始我用前端定时轮询订单状态接口5秒查一次虽然能用但是体验差——订单来了最多延迟5秒才知道而且频繁请求浪费资源。后来改成WebSocket后端一有新订单就主动推送给连接的后厨端秒级响应。SpringBoot整合WebSocket不算复杂核心三步引入依赖、配置端点、写消息处理器。Component ServerEndpoint(/ws/kitchen) Slf4j public class KitchenWebSocket { private static CopyOnWriteArraySetSession sessions new CopyOnWriteArraySet(); OnOpen public void onOpen(Session session) { sessions.add(session); log.info(后厨端连接当前连接数{}, sessions.size()); } OnClose public void onClose(Session session) { sessions.remove(session); } OnError public void onError(Session session, Throwable error) { log.error(WebSocket错误, error); } public static void sendNewOrderMessage(String message) { for (Session session : sessions) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { log.error(推送消息失败, e); } } } }下单成功后在Service层调用KitchenWebSocket.sendNewOrderMessage(...)把订单编号和桌台号推送出去。前端用Vue的new WebSocket(ws://localhost:8080/ws/kitchen)接收消息监听到新订单后刷新列表并播放提示音。这里有个部署坑如果后端用了Nginx反向代理WebSocket需要单独配置升级头否则会报“握手失败”。Nginx配置如下location /ws/ { proxy_pass http://127.0.0.1:8080; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; }3.5 统一返回与全局异常处理前后端分离开发中最烦人的是接口返回格式不统一。有人返回{code: 200, data: ...}有人直接返回裸JSON前端接数据时各种判断极易出bug。我在这个项目里做了统一响应体。Data public class ResultT { private Integer code; // 200成功其他失败 private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }全局异常处理用RestControllerAdvice配合ExceptionHandler把参数校验异常、业务异常、未知异常统一包装成Result格式返回前端就不用关心各种异常栈信息了。这个环节看起来很基础但实际开发中能省掉大量联调时间。4. 前端Vue实现顾客H5与管理后台的双前端架构4.1 工程结构一个仓库还是两个项目这个系统前端我实际拆成了两个工程restaurant-h5顾客扫码点餐端和restaurant-admin商家管理端。虽然两者共用后端API但界面风格、交互逻辑、路由结构完全不同硬塞在一个Vue工程里会让代码越来越乱。管理员端用Vue3 Element Plus Vite开发顾客H5考虑到微信内置浏览器兼容性我用的是Vue3 Vant组件库。为什么用Vant因为Vant本身就是移动端UI库有现成的底部导航、商品卡片、弹出层、购物车栏做点餐页面几乎就是拼积木。Vue环境配置是很多新手卡住的点我提一句先用Node 18版本太低太高都可能有兼容性问题然后npm create vuelatest创建工程装依赖npm install开发npm run dev构建npm run build。Vite冷启动速度比Webpack快太多了强烈建议不要再用老掉牙的Vue CLI。4.2 Axios封装与请求拦截前后端分离必须封装Axios不能每个组件里this.$http.get一把梭。我在src/utils/request.js里做了一个统一配置import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, // 开发环境通过Vite代理生产环境通过Nginx转发 timeout: 10000 }) // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error)) // 响应拦截器统一处理错误 request.interceptors.response.use(response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { if (error.response.status 401) { localStorage.removeItem(token) router.push(/login) } ElMessage.error(error.message || 网络异常) return Promise.reject(error) }) export default request这个封装的关键点在于后端返回的body统一是{code, message, data}结构拦截器里先判断code成功直接返回data给页面失败统一弹出错误提示。这样Vue组件里写接口调用就是一个干净的三行代码const res await request.get(/dish/list) this.dishList res4.3 点餐页面核心逻辑购物车与桌台参数顾客端的核心页面是菜单列表 商品详情 购物车弹层。Vant组件里van-card放菜品卡片van-stepper做数量加减购物车用van-popup弹出一套组合拳下来页面就成型了。购物车状态我在项目里用的是PiniaVue 3官方推荐状态库比Vuex更轻量。因为购物车数据要跨组件共享菜单页加购、购物车弹层改数量、结算页读数据没有全局状态管理根本没法做。// stores/cart.js import { defineStore } from pinia export const useCartStore defineStore(cart, { state: () ({ items: [] // [{ dishId, dishName, price, image, quantity }] }), getters: { totalCount: state state.items.reduce((sum, item) sum item.quantity, 0), totalPrice: state state.items.reduce((sum, item) sum item.price * item.quantity, 0) }, actions: { addItem(dish) { const existing this.items.find(item item.dishId dish.id) if (existing) { existing.quantity } else { this.items.push({ dishId: dish.id, dishName: dish.name, price: dish.price, image: dish.image, quantity: 1 }) } }, removeItem(dishId) { this.items this.items.filter(item item.dishId ! dishId) }, clear() { this.items [] } } })桌台参数传参有几个方案URL参数、二维码携带、路由query。我实际用的是二维码方式每个桌台的二维码内容是一个URL比如https://xxx.com/h5?tableId12。顾客扫码进入后Vue路由守卫里读取route.query.tableId存到Pinia或SessionStorage下单时把tableId传给后端。这种设计有一个必须考虑的边界顾客扫码后如果刷新页面query参数会丢失。所以我在进入页面时先把tableId存到SessionStorage后续刷新也能读到。别用localStorage因为顾客换桌或者重扫后旧桌号会被错误带过去Session在当前标签页生命周期内保留更合理。4.4 路由权限与动态路由管理后台的路由权限可以做得简单也可以做得很复杂。我这个项目里的方案是静态路由 路由守卫校验登录状态 菜单按角色动态渲染没有做后端返回动态路由表的高级玩法因为餐厅角色就三种权限差异很小做动态路由属于过度设计。路由守卫示例router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path /login) { next() return } if (!token) { next(/login) return } if (to.meta.roles !to.meta.roles.includes(store.user.role)) { next(/403) return } next() })这里要注意Vue路由有两种模式hash和history。开发时用默认的hash模式没有太多坑但生产环境如果要用history模式Nginx必须做try_files配置否则刷新页面就会404。我管理后台用的history模式Nginx配置如下location / { try_files $uri $uri/ /index.html; }顾客H5因为在微信内打开hash模式反而更省心不容易出现奇怪的刷新问题我就保留了hash模式。4.5 菜品管理与图片上传管理后台的菜品管理页面是典型CRUD表格展示、新增/编辑弹窗、删除确认、分页搜索。图片上传这里我踩过一个较大的坑一开始直接把图片Base64编码存数据库一张几MB的图片就能把数据库撑爆接口响应慢到崩溃。后来改成文件上传方案前端用Element Plus的el-upload组件把图片传到后端后端保存到本地磁盘或云存储数据库只存访问路径。刚开始我用本地磁盘存储部署后图片路径写死成本机地址换服务器就全部失效。后来我把图片存储抽出来用MinIO做对象存储Docker一行命令就能启动和SpringBoot整合也很顺畅。PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file) { if (file.isEmpty()) { return Result.error(上传文件不能为空); } String fileName UUID.randomUUID().toString() . StringUtils.getFilenameExtension(file.getOriginalFilename()); // 存储文件 try { file.transferTo(new File(uploadPath fileName)); return Result.success(fileName); } catch (IOException e) { log.error(文件上传失败, e); return Result.error(上传失败); } }图片存储这一块选择空间很大本地磁盘、MinIO、阿里云OSS、腾讯云COS都可以。毕设和课程设计就用本地磁盘或MinIO最简单商用项目建议直接上云OSS省去自己处理备份和CDN的麻烦。5. 环境配置与部署上线从本地跑通到服务器落地5.1 开发环境配置的核心要点先把开发环境踩坑清单列一下数据库连接application.yml里spring.datasource.url必须加useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai否则中文乱码、时间差8小时的问题全来了。spring: datasource: url: jdbc:mysql://localhost:3306/restaurant?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai redis: host: localhost port: 6379端口冲突SpringBoot默认8080如果本地已经被占用可以在配置里写server.port: 8081或者用随机端口server.port: 0但调试时不建议每次启动端口都在变。热部署这属于开发体验改善项spring-boot-devtools可以实现修改代码后自动重启但注意它和某些实体类缓存的兼容问题。如果你想省事直接用IDEA的JRebel或者干脆手动重启也完全没问题。5.2 前端联调解决跨域问题开发环境跨域是前后端分离项目第一个拦路虎。前端跑在5173端口后端跑在8080端口直接Ajax请求必然报跨域。方案一后端配置CORS。SpringBoot里写一个配置类允许指定前端地址跨域访问。这个方案能用但生产环境每加一个域名就得改一次不够优雅。方案二推荐开发环境用Vite代理。在vite.config.js里配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: path path.replace(/^\/api/, ) } } } })前端代码里所有接口都写成/api/...开头Vite开发服务器帮你去请求后端浏览器端不存在跨域。生产环境把baseURL换成Nginx反向代理的路径就行后端接口不用改前端代码也不用改。5.3 生产部署Jar包 Nginx Docker后端打包成可执行Jar包mvn clean package -DskipTests java -jar restaurant-server.jar --spring.profiles.activeprod生产环境我推荐用Docker部署省去安装JDK、MySQL、Redis的麻烦。写一个简单的DockerfileFROM openjdk:8-jre-alpine COPY restaurant-server.jar /app.jar EXPOSE 8080 ENTRYPOINT [java, -jar, /app.jar]构建镜像命令docker build -t restaurant-server . docker run -d -p 8080:8080 --name restaurant-server restaurant-server前端构建npm run build把生成的dist目录丢到Nginx的html目录加上反向代理配置一个完整可访问的系统就上线了。这里包含一个常常被忽略的细节后端接口路径如果带/api前缀Nginx代理时要一起转发不能把前缀截断否则后端路由找不到对应接口。6. 常见问题与排查技巧实录6.1 LocalDateTime序列化问题后端实体类用LocalDateTime而不是Date返回给前端时默认序列化格式是一长串数字时间戳或者变成2025-01-01T10:00:00带T的ISO格式前端显示非常丑。解决方案有两种一种在实体字段上加JsonFormat(pattern yyyy-MM-dd HH:mm:ss)另一种全局配置Jacksonspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai注意全局配置对LocalDateTime不生效必须单独注册Jackson的自定义序列化器使用JavaTimeModule时DateFormat不覆盖LocalDateTime。我在项目里写了一个配置类注册LocalDateTimeSerializerConfiguration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - { builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; } }6.2 接口报400/401/404的排查思路联调阶段最烦的就是接口各种状态码400 Bad Request多半是参数类型不匹配比如后端要Long前端传了字符串。优先看控制台日志里具体的类型转换异常。401 UnauthorizedToken没带或者过期。检查请求拦截器是否正确加上Authorization头检查JWT密钥是否一致开发环境和生产环境密钥不同也会突然全部401。404 Not Found后端路径没对上。SpringBoot Controller的RequestMapping路径与前端请求路径必须完全一致大小写、斜杠都不能错。6.3 并发测试多用户同时下单怎么办单机开发时很难暴露并发问题但餐厅场景中午高峰期肯定有多桌同时下单。我用JMeter做过简单的并发测试发现两个高频问题第一扣库存超卖这个前面已经用乐观锁SQL解决了。第二数据库连接池耗尽。默认HikariCP连接池最大连接数只有10并发高时容易报connection is not available。在配置里适当调大spring: datasource: hikari: maximum-pool-size: 50 minimum-idle: 10第三订单号重复问题在order_no上建唯一索引后手动捕获DuplicateKeyException并重新生成订单号即可。我实际的做法是订单号用“yyyyMMddHHmmss 3位随机数 userId后4位”撞单概率几乎为零但以防万一还是加了唯一索引。6.4 前端移动端适配与微信内置浏览器问题顾客H5要考虑多种手机屏幕我用了Vant之后基本能自适应。但有几个坑是微信内置浏览器特有的微信内置浏览器不支持像普通浏览器那样直接唤起微信支付必须走微信支付的JS-SDK流程需要公众号AppID和商户号。毕设阶段我直接用“模拟支付”按钮跳转过去真实商用再接入微信支付。如果后续要在H5里做视频展示比如后厨直播或者菜品制作过程微信内置浏览器的视频播放有x5-playsinline属性要求M3U8格式的直播流在 iOS 上支持较好Android 端经常需要自定义播放器。这不是点餐系统核心我暂时没深入研究提一句供有需求的人参考。如果涉及配送定位以后扩展外卖场景可以用腾讯地图JS SDK在H5里做定位和路径展示但注意需要在腾讯地图开放平台申请Key并且域名要提前配置白名单。6.5 数据库时区问题这真的是个经典老坑。数据库连接串里没有设置serverTimezone时插入的时间比实际时间少8个小时。我排查时第一反应是Java代码写错了后来才发现是MySQL连接串少了时区参数。解决方式就是在JDBC URL里加上serverTimezoneAsia/Shanghai并且在实体类中统一使用LocalDateTime类型不要混用java.util.Date和java.sql.Timestamp混用会导致时间在很多框架里被重复转换结果错上加错。6.6 后端启动失败与版本兼容性问题SpringBoot版本和JDK版本不匹配是新手常见问题。比如SpringBoot 3.x要求JDK 17以上你用JDK 8跑就必然报错。要是你手头JDK版本太低就老老实实用SpringBoot 2.7.x不要盲目追求新版本。还有一类问题是依赖版本冲突典型的是MyBatis-Plus和SpringBoot版本不兼容导致启动时Invalid bound statement。我建议使用MyBatis-Plus的spring-boot-starter时选择与SpringBoot版本匹配的MP版本比如SpringBoot 2.7对应MyBatis-Plus 3.5.xSpringBoot 3.x对应MyBatis-Plus 3.5.5。7. 扩展思路像真实项目一样继续演进如果你做完核心功能还想加分这里有几个方向可以选按投入产出比排序接入真实支付微信支付 Native 扫码支付顾客在H5下单后调起微信支付收银台。难度中等需要注册商户号文档齐全做出来简历含金量高。打印机对接后厨58mm热敏小票打印机通过云打印服务如飞鹅云实现下单自动打印小票。成本低体验好很多小餐馆极其需要。数据统计图表化管理后台用ECharts做营业额折线图、菜品销量排行饼图、客流高峰时段分析。代码量不大但视觉冲击力强答辩时也更好讲。会员积分系统消费累计积分积分抵扣。需要加积分流水表业务逻辑难度不大但对进销存的理解能提升一个档次。8. 最后的经验分享我前前后后做过三套餐厅点餐系统第一套从数据库建模开始就是乱的订单状态想到哪加到哪最后统计营业额时不是多算就是漏算。第二套学乖了先把状态机画清楚把“待支付到已支付、已支付到制作中、制作中到已完成”的流转边界固定下来后面所有代码都跟着状态机走业务就稳了大半。还有一点做这类全栈项目别想着一步到位。我的习惯是先跑通“扫码 - 看菜单 - 加购 - 下单 - 后厨看到订单”这个最核心的闭环再回头补管理系统、统计报表、权限这些辅助功能。核心链路通了信心就有了后面就是锦上添花。另外如果你拿到了一个老项目源码想改成自己的毕设或者商用项目可以直接把Jar包用工具反编译成工程文件市面上有 JD-GUI、Luyten以及IDEA自带的反编译插件但反编译出来代码结构可能丢注释实体关系得自己慢慢理。前提是你对这个项目有合理的使用授权不要拿别人的代码直接提交说成原创该懂得避雷还是得懂。最后还是那句话做项目遇到问题不可怕多打日志、多翻官方文档、多在调试器里断点走一遍百分之八十的问题都是自己写的时候没想清楚。这套点餐系统的技术栈不算新但把每一个模块都做扎实了你收获的就是一套真实可用的企业项目经验远超背一百道面试题。
返回列表