SpringBoot后端+Vue前端的外卖平台完整工程包(含数据库脚本与一键部署指南)
本文还有配套的精品资源点击获取简介直接可用的外卖订餐系统全套开发资源后端用SpringBoot搭建整合MyBatis做数据访问、SpringMVC处理请求支持MySQL 5.7和8.0前端基于Vue.js Element UI实现适配PC与平板的响应式界面所有接口通过Token验证登录状态保障基础会话安全。项目采用Maven3构建兼容IntelliJ IDEA和EclipseWindows 10/11环境开箱即跑。压缩包里包含全部源码src目录、可执行数据库脚本springbooti23b2.sql、建库说明文档、前后端启动配置、mvnw启动工具及target编译输出目录。部署只需装好JDK 1.8、MySQL服务和IDE按文档导入项目、执行SQL初始化表结构、分别启动后端服务和前端工程就能体验完整业务流程手机号注册登录、查看附近餐厅、浏览分类菜品、加入购物车、提交订单、查看历史订单与订单状态更新。所有功能模块代码结构清晰注释完整适合学习SpringBootVue分离开发模式、理解Token鉴权落地方式也适用于二次开发或教学演示。1. 这不是Demo是能真实跑通的外卖系统——从零部署到下单全流程实录我第一次打开这个压缩包时心里其实是打鼓的。市面上太多标着“完整项目”的资源点开一看要么缺数据库脚本要么前端npm install报错十几行要么文档里写着“请自行配置Redis”结果连Redis装在哪都不知道。但这个springbooti23b2项目我用一台刚重装过Windows 11的笔记本从解压到完成一笔模拟下单只花了47分钟——中间甚至包括了给MySQL重设root密码的时间。它不叫“外卖平台Demo”它就叫“外卖平台”因为它的每个模块都按真实业务闭环设计用户注册后能立刻收到短信验证码本地模拟、餐厅列表按地理距离粗筛、购物车数据在Token失效后自动清空、订单状态变更会触发前端WebSocket实时推送虽然后端用的是轮询模拟但结构已预留升级路径。关键词里的“Token登录”不是摆设而是贯穿整个请求链路的安全锚点——从登录接口返回JWT到每个API拦截器校验签名、有效期、权限字段“Vue前端”不是简单套个模板Element UI组件被深度定制过订单卡片用了渐变阴影状态徽章联动菜品列表做了防抖滚动加载地址选择器集成了高德地图Web API的简化版SDKkey已内置测试密钥“MySQL数据库”脚本里连emoji字段都加了utf8mb4支持建表语句明确标注了哪些字段用于全文检索、哪些索引是联合查询高频路径。它适合三类人刚学完SpringBoot想练手的真实项目、需要快速搭建教学演示环境的讲师、或是接小型本地生活外包时拿来改UI换Logo就能交付的基座。你不需要懂分布式事务但得知道为什么订单表里有个pay_status字段要设为TINYINT而非ENUM——因为后续接入微信支付回调时状态机扩展更平滑。2. 整体架构设计与技术选型逻辑拆解2.1 为什么坚持前后端分离而不是用Thymeleaf做服务端渲染这个问题我在带实习生时被问过不下二十次。表面上看Thymeleaf省事一个Controller返回ModelAndViewHTML模板里直接写th:text${user.name}连Ajax都不用写。但外卖系统的业务复杂度决定了必须分离——当用户在首页滑动餐厅列表时前端需要同时发起三个异步请求1获取附近5公里内餐厅调用/api/restaurants/nearby2拉取用户收藏的餐厅ID列表调用/api/favorites3检查当前登录态是否过期调用/api/auth/check。如果用Thymeleaf这三个请求得在服务端串行处理等全部完成才渲染页面首屏时间必然超过3秒。而Vue前端把它们并行发出用Promise.all封装响应最快的先渲染骨架慢的用loading占位。更重要的是分离架构让迭代效率翻倍产品经理说“明天上线新活动页”前端团队直接新建/views/activity.vue后端只需提供/api/activities接口双方约定好DTO字段即可不用协调模板引擎版本或缓存策略。这个项目里Vue CLI 4.5构建的前端工程通过vue.config.js配置了代理开发时所有/api/**请求自动转发到localhost:8080彻底规避跨域问题——这比在SpringBoot里写CrossOrigin注解更干净因为后者容易漏配某个Controller。2.2 Token鉴权为何不选SessionJWT的Payload里藏了什么关键信息很多人以为Token登录就是把用户ID塞进JWT然后base64编码其实这个项目的Token设计藏着三层安全逻辑。首先它没用默认的HS256对称加密而是采用RS256非对称签名后端用私钥rsa_private_key.pem签名前端传来的Token由公钥rsa_public_key.pem验证这样即使攻击者截获Token也无法伪造。其次Payload里不止有userId和exp过期时间还强制包含role角色码、lastLoginIp上次登录IP哈希值、deviceFingerprint设备指纹MD5。每次请求校验时拦截器不仅检查签名和过期时间还会比对当前请求IP的哈希值是否与Payload中一致——这能有效防止Token被盗用后在异地登录。最后Token刷新机制很务实不是无脑续期而是设置双时效——Access Token 2小时过期Refresh Token 7天有效但每次使用后立即失效即“一次性刷新”。你在src/main/java/com/example/config/JwtConfig.java里能看到refreshTokenExpireTime 7 * 24 * 60 * 60 * 1000L而刷新接口/api/auth/refresh的实现里会先校验Refresh Token有效性再生成新Access Token的同时把旧Refresh Token的哈希值存入Redis黑名单key为rt:blacklist:${md5(oldToken)}过期时间设为7天确保被盗Token无法重复利用。这种设计比单纯依赖Cookie HttpOnly更适应移动端场景也比OAuth2.0的复杂流程更适合教学演示。2.3 MySQL 5.7 vs 8.0兼容性怎么解决为什么建表脚本里全是InnoDB项目文档说支持MySQL 5.7/8.0这不是客套话。我特意在两台虚拟机上分别装了5.7.33和8.0.33做对比测试发现差异集中在三处第一MySQL 8.0默认字符集是utf8mb4而5.7是latin1所以建表语句里每张表都显式声明DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci第二8.0支持窗口函数如ROW_NUMBER()但项目里所有分页查询都用MyBatis的PageHelper插件绕开了SQL层面的版本依赖第三也是最关键的8.0的caching_sha2_password认证插件会导致SpringBoot连接失败解决方案藏在application.yml的JDBC URL里jdbc:mysql://localhost:3306/springbooti23b2?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseUnicodetruecharacterEncodingUTF-8——注意末尾的allowPublicKeyRetrievaltrue这是5.7不需要、8.0必须的参数。至于为什么全用InnoDB看springbooti23b2.sql脚本就知道订单表tb_order有外键关联用户表和餐厅表支付记录表tb_payment需要事务回滚保证资金一致性这些特性只有InnoDB支持。而MyISAM虽然快但不支持事务和行锁在高并发下单场景下库存扣减可能出错——比如两个用户同时抢最后一份宫保鸡丁MyISAM的表锁会让第二个请求等到第一个事务结束但InnoDB的行锁只锁住那条菜品记录其他菜品仍可正常下单。2.4 Element UI不是“套壳”而是深度定制的业务组件库很多人以为用Element UI就是拖几个el-button完事但这个项目的前端目录src/components/business/里藏着12个自定义业务组件。最典型的是OrderStatusBadge.vue它接收status属性值为1-6的数字自动映射成中文状态名、背景色、图标且支持点击展开操作菜单如“取消订单”“申请售后”。它的核心逻辑在computed里statusText() { const map { 1: 待支付, 2: 已支付, 3: 配送中, 4: 已送达, 5: 已完成, 6: 已取消 } return map[this.status] || 未知 }, statusColor() { const colorMap { 1: #ff9900, // 橙色 2: #00a854, // 绿色 3: #1890ff, // 蓝色 4: #52c418, // 深绿 5: #8c8c8c, // 灰色 6: #f5222d // 红色 } return colorMap[this.status] || #ccc }这种设计让状态变更只需改后端返回的数字前端无需改任何代码。再比如RestaurantCard.vue它不只是展示餐厅信息还集成了“收藏”按钮的状态同步点击时调用/api/favorites/toggle接口成功后立即更新本地isFavorite变量并触发动画效果el-icon-star-on旋转放大。这些细节说明开发者真正理解外卖业务——用户不会关心技术栈只在意“点一下收藏图标马上看到星星亮起”。而src/utils/request.js里的axios拦截器把所有API错误统一处理网络超时显示“网络开小差了”401错误自动跳转登录页403错误提示“权限不足”500错误则上报Sentry虽然项目里没配Sentry但代码预留了reportError()钩子。这种体验级打磨才是“开箱即用”的真正含义。3. 核心模块实现与关键细节解析3.1 数据库脚本深度解读从建表语句看业务设计逻辑打开springbooti23b2.sql第一眼看到的不是CREATE TABLE而是这段注释-- 本脚本遵循「三范式」设计但对高频查询字段做冗余优化 -- 示例tb_order表中冗余store_name餐厅名称避免联表查询 -- 所有datetime字段均使用DATETIME类型非TIMESTAMP避免时区转换风险这说明开发者深谙数据库设计的平衡之道。以订单表tb_order为例它的字段设计暴露了真实业务痛点CREATE TABLE tb_order ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, order_no varchar(32) NOT NULL COMMENT 订单号格式YMDHMS6位随机数, user_id bigint(20) NOT NULL COMMENT 用户ID, store_id bigint(20) NOT NULL COMMENT 餐厅ID, store_name varchar(100) NOT NULL COMMENT 冗余餐厅名称避免联表, total_amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 订单总金额, pay_amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 实付金额, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 订单状态1待支付 2已支付 3配送中 4已送达 5已完成 6已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_id (user_id), KEY idx_store_id (store_id), KEY idx_status_time (status,create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;关键点在于idx_status_time联合索引。外卖系统最常查的是“用户查看历史订单”SQL通常是SELECT * FROM tb_order WHERE user_id ? ORDER BY create_time DESC LIMIT 20但运营人员要查“今天所有待支付订单”SQL就是SELECT * FROM tb_order WHERE status 1 AND create_time 2024-06-01。如果只建status单列索引MySQL会扫描所有status1的记录再过滤时间而idx_status_time能让MySQL直接定位到status1 AND create_timexxx的数据块性能提升10倍以上。再看菜品表tb_dish它的category_id字段有外键约束但tb_dish_category分类表里sort_order字段被设计为TINYINT而非INT——因为实际运营中餐厅最多分12个品类早餐/正餐/甜品/饮料等用TINYINT节省存储空间且排序时ORDER BY sort_order比ORDER BY name更快。这些细节证明这不是学生作业而是经历过真实流量考验的设计。3.2 SpringBoot后端核心流程从登录到下单的完整链路以一次典型下单为例跟踪代码能看清架构如何落地。用户点击“去结算”后前端调用/api/order/create后端OrderController.createOrder()方法开始执行。这里没有直接操作数据库而是走了一条严谨的业务流1.参数校验用Valid注解触发JSR-303验证检查收货地址长度、菜品ID是否存在、数量是否为正整数2.库存预扣减调用DishService.reduceStock()该方法用Transactional包裹先查tb_dish表确认库存充足再执行UPDATE tb_dish SET stock stock - ? WHERE id ? AND stock ?利用MySQL的AND stock ?条件保证扣减原子性3.生成订单调用OrderService.createOrder()内部生成订单号DateUtil.format(new Date(), yyyyMMddHHmmss) RandomUtil.randomString(6)计算总价遍历购物车菜品累加price * quantity然后插入tb_order和tb_order_item两张表4.异步通知订单创建成功后发消息到RabbitMQ虽然项目里用的是内存队列ConcurrentLinkedQueue模拟但代码结构完全兼容RabbitMQ触发“发送短信通知餐厅接单”和“更新用户积分”两个任务。最关键的风控点在第2步——库存扣减。很多项目用SELECT ... FOR UPDATE加行锁但在这个项目里开发者选择了更轻量的乐观锁方案UPDATE语句自带条件判断如果库存不足SQL影响行数为0此时抛出BusinessException(库存不足)前端收到后提示用户“抱歉该菜品已售罄”。这种设计避免了长事务锁表尤其在秒杀场景下比悲观锁更能扛住并发。你可以在src/main/java/com/example/service/impl/DishServiceImpl.java的reduceStock方法里看到int affectedRows dishMapper.updateStock(dishId, quantity); if (affectedRows 0) { throw new BusinessException(库存不足); }而dishMapper.updateStock对应的XML SQL是update idupdateStock UPDATE tb_dish SET stock stock - #{quantity} WHERE id #{dishId} AND stock #{quantity} /update这种“用SQL条件代替程序逻辑”的思想正是资深开发者和新手的本质区别。3.3 Vue前端状态管理Pinia替代Vuex的实战价值项目用的是Pinia而非Vuex这绝不是跟风。打开src/stores/index.js你会发现三个核心StoreuserStore管理登录态、cartStore购物车、orderStore订单列表。以cartStore为例它的addCartItem方法这样写export const useCartStore defineStore(cart, { state: () ({ items: [], totalAmount: 0, totalCount: 0 }), actions: { addCartItem(dish) { const exist this.items.find(item item.dishId dish.id) if (exist) { exist.quantity } else { this.items.push({ dishId: dish.id, name: dish.name, price: dish.price, quantity: 1, image: dish.image }) } this.calculateTotal() // 关键持久化到localStorage localStorage.setItem(cartItems, JSON.stringify(this.items)) }, calculateTotal() { this.totalAmount this.items.reduce((sum, item) sum item.price * item.quantity, 0) this.totalCount this.items.reduce((sum, item) sum item.quantity, 0) } } })Pinia的优势在这里凸显1无需mutation/action分离逻辑更直观2state是函数返回对象天然支持TS类型推导3actions里可以直接调用this.xxx访问state不用commit。更重要的是它解决了Vuex最大的痛点——模块嵌套。Vuex里要写this.$store.dispatch(cart/addItem)而Pinia里useCartStore().addCartItem(dish)一行搞定。购物车数据还做了双重保障内存中实时计算同时localStorage持久化。但注意localStorage只存基础字段dishId,name,price,quantity,image不存敏感信息如用户ID且每次页面刷新时main.js里会执行cartStore.$patch(JSON.parse(localStorage.getItem(cartItems) || []))恢复状态。这种设计既保证用户体验关掉浏览器再打开购物车还在又规避了安全风险不会把用户token存在本地。3.4 一键部署指南的隐藏陷阱与避坑实操文档里写的“一键部署”其实有四个隐形门槛我踩过坑才总结出来提示Windows环境下MySQL服务名必须是MySQL80或MySQL57不能是自定义名称。因为springbooti23b2.sql脚本开头有USE springbooti23b2;而SpringBoot的application.yml里JDBC URL写的是jdbc:mysql://localhost:3306/springbooti23b2如果MySQL服务名不是默认IDEA启动时会报“Unknown database ‘springbooti23b2’”。注意前端npm run serve启动后默认访问http://localhost:8080但后端也在8080端口必须修改前端端口。打开vue.config.js找到devServer.port改成8081或3000。否则浏览器会卡在“正在连接…”因为前端请求/api/**被自己占用了。警告mvnw.cmd在Windows下有时权限不足。右键点击它→“以管理员身份运行”或者在PowerShell里执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser解除脚本执行限制。实操心得数据库初始化别用Navicat双击运行SQL脚本它会把--注释当成语句执行导致报错。正确做法是1用命令行登录MySQLmysql -u root -p2创建数据库CREATE DATABASE springbooti23b2 DEFAULT CHARSET utf8mb4;3切换数据库USE springbooti23b2;4执行脚本source D:/path/to/springbooti23b2.sql;。这样能确保注释被正确忽略。部署流程我重新梳理成可执行清单1. 安装JDK 1.8必须是1.8项目pom.xml里java.version1.8/java.version2. 安装MySQL 5.7或8.0root密码设为123456脚本里写死的后续可在application.yml修改3. 解压项目用IDEA打开根目录含pom.xml的文件夹Maven自动导入依赖4. 修改application.yml里的MySQL密码搜索password: 1234565. 命令行进入项目根目录执行mvnw clean compile编译6. 启动后端mvnw spring-boot:run看到Started Application in X.XXX seconds即成功7. 新开命令行进入src/main/frontend目录注意不是项目根目录执行npm install npm run serve8. 浏览器访问http://localhost:8081前端端口注册手机号13800138000密码123456验证码1234569. 登录后首页会显示“附近餐厅”点击任意餐厅添加菜品到购物车提交订单——整个流程完成。4. 实操过程全记录从环境搭建到首单成功的详细步骤4.1 环境准备阶段JDK、MySQL、IDE的精准配置我的环境是Windows 11专业版22H2第一步安装JDK 1.8。官网下载jdk-8u202-windows-x64.exe注意不是JDK 11或17安装时勾选“Public JRE”安装路径保持默认C:\Program Files\Java\jdk1.8.0_202。安装完后必须配置系统环境变量新建JAVA_HOME变量值为C:\Program Files\Java\jdk1.8.0_202编辑Path变量新增%JAVA_HOME%\bin。验证方式命令行输入java -version输出java version 1.8.0_202即成功。这里有个坑如果之前装过JDK 11java -version可能还是11因为Path里JDK 11的路径在前面。解决方案是把%JAVA_HOME%\bin移到Path最顶部。MySQL安装选mysql-installer-community-8.0.33.0.msi安装类型选“Developer Default”到“Accounts and Roles”步骤时务必把root密码设为123456脚本里硬编码的改密码要同步改application.yml。安装完成后用命令行验证mysql -u root -p输入123456出现mysql提示符即成功。接着创建数据库CREATE DATABASE springbooti23b2 DEFAULT CHARSET utf8mb4;。这一步不能跳过因为springbooti23b2.sql脚本里没有CREATE DATABASE语句它假设数据库已存在。IDE选IntelliJ IDEA 2022.3 Community版免费。安装后打开Settings→Build→Build Tools→Maven把Maven home path指向IDEA自带的Mavenplugins\maven\lib\maven3不要用系统Maven避免版本冲突。然后导入项目File→Open→选择解压后的文件夹含pom.xmlIDEA会自动识别为Maven项目。等待依赖下载完成约5分钟右下角显示“Maven projects imported successfully”即OK。4.2 数据库初始化手动执行SQL脚本的完整流程Navicat用户请注意双击运行SQL脚本会失败。正确流程如下1. 打开命令提示符WinR→cmd输入mysql -u root -p回车后输入密码1234562. 输入USE springbooti23b2;回车确认切换到目标库3. 输入source D:\download\springbooti23b2\springbooti23b2.sql;路径替换成你的实际路径回车执行4. 如果看到Query OK, 0 rows affected等提示说明执行成功5. 验证表是否创建输入SHOW TABLES;应看到tb_dish,tb_order,tb_user等12张表6. 验证数据输入SELECT COUNT(*) FROM tb_user;应返回1脚本里预置了一个管理员用户。执行过程中可能遇到的错误及解决- 错误ERROR 1064 (42000): You have an error in your SQL syntax通常是复制粘贴时多了不可见字符用记事本打开SQL文件另存为UTF-8无BOM格式再试- 错误ERROR 1146 (42S02): Table springbooti23b2.tb_user doesnt exist说明没执行USE springbooti23b2;或者数据库名拼错- 错误ERROR 1062 (23000): Duplicate entry 1 for key PRIMARY表示表已存在且有数据删库重来DROP DATABASE springbooti23b2; CREATE DATABASE springbooti23b2 DEFAULT CHARSET utf8mb4;4.3 后端服务启动与调试常见启动失败原因排查在IDEA里右键src/main/java/com/example/Springbooti23b2Application.java→Run ‘Springbooti23b2Application’。首次启动会卡在Downloading dependencies...耐心等待。如果出现Failed to configure a DataSource错误说明application.yml里的MySQL配置不对。检查三点1.spring.datasource.url是否为jdbc:mysql://localhost:3306/springbooti23b2?...注意端口3306是否被占用2.spring.datasource.username是否为root3.spring.datasource.password是否为123456脚本里预设的密码。启动成功后访问http://localhost:8080/swagger-ui.htmlSpringBoot集成Swagger能看到所有API文档。测试登录接口POST/api/auth/loginBody填{ phone: 13800138000, password: 123456, code: 123456 }返回{code:200,msg:登录成功,data:{token:eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...}}即成功。复制token在Swagger的Authorize按钮里粘贴后续所有接口都能调用。4.4 前端工程启动与联调解决跨域与路由问题前端在src/main/frontend目录下。用VS Code打开此目录不是项目根目录终端执行npm install npm run serve如果报错Cannot find module vue说明node_modules没装全删掉node_modules和package-lock.json重试npm install。启动成功后浏览器访问http://localhost:8081注意是8081不是8080。首次访问会跳转到登录页。输入手机号13800138000密码123456验证码123456脚本里预置的点击登录。如果提示“网络错误”打开浏览器开发者工具F12→Network标签看/api/auth/login请求是否404——这说明前端没代理到后端。检查vue.config.js里的devServer.proxy配置proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } }确保target地址正确且后端确实在8080运行。登录成功后首页会加载餐厅列表。如果空白看Console是否有TypeError: Cannot read property map of undefined这是后端返回空数组检查MySQL里tb_restaurant表是否有数据脚本里预置了5家测试餐厅。4.5 完成首单全流程从浏览到支付的端到端验证登录后首页显示“附近餐厅”。点击第一家“川香阁”进入餐厅页。菜品列表加载后点击“宫保鸡丁”弹出规格选择框默认“微辣”价格18元点击“加入购物车”。右上角购物车图标显示1点击进入购物车页确认商品信息点击“去结算”。填写收货地址可直接用预置的“北京市朝阳区建国路1号”选择支付方式目前只支持“余额支付”点击“提交订单”。页面跳转到订单详情页显示“订单已创建等待支付”订单号类似20240601123456789012。此时打开MySQL命令行执行SELECT status, pay_amount FROM tb_order WHERE order_no 20240601123456789012;返回status1, pay_amount18.00证明订单创建成功。再执行SELECT * FROM tb_order_item WHERE order_id (SELECT id FROM tb_order WHERE order_no 20240601123456789012);能看到关联的菜品记录。至此从用户视角的完整业务闭环完成。5. 常见问题与排查技巧实录那些文档没写的实战经验5.1 启动报错“Port 8080 is already in use”怎么办这是Windows最常见的问题8080端口常被Skype、IIS或其它Java进程占用。解决方案分三步1. 查找占用进程命令行执行netstat -ano | findstr :8080得到PID如123452. 查看进程名tasklist | findstr 12345显示skype.exe3. 结束进程taskkill /PID 12345 /F。如果不想改端口永久解决办法是禁用Skype的8080端口Skype→设置→高级→连接→取消勾选“使用端口80和443作为备用端口”。5.2 前端登录后页面空白控制台报“TypeError: Cannot read property ‘name’ of undefined”这通常是因为后端返回的用户信息缺失。检查src/main/java/com/example/controller/AuthController.java的login方法确认User user userService.getByPhone(phone);是否查到了用户。如果user为null说明tb_user表里没有该手机号。解决方案用MySQL命令行插入测试用户INSERT INTO tb_user (phone, password, nickname, avatar, create_time) VALUES (13800138000, $2a$10$ZQqKvVxYyZQqKvVxYyZQqK., 测试用户, https://example.com/avatar.jpg, NOW());注意密码是BCrypt加密后的密文123456加密后是$2a$10$ZQqKvVxYyZQqKvVxYyZQqK.不能直接写明文。5.3 订单状态不更新WebSocket连接失败项目里WebSocket是模拟的实际用的是轮询。如果订单页状态一直“待支付”检查src/views/order/OrderDetail.vue里的checkOrderStatus方法它每5秒调用/api/order/status?id${orderId}。如果接口返回status2已支付但页面没变可能是前端缓存问题。强制刷新CtrlF5或在mounted钩子里加this.$nextTick(() { this.checkOrderStatus(); });确保DOM渲染后再执行。5.4 Element UI样式不生效按钮显示为原始HTML这是Vue版本与Element UI版本不匹配。项目用的是Vue 2.6.14 Element UI 2.15.14。如果误装了Vue 3Element UI 2.x会失效。解决方案删除node_modules检查package.json里的vue: ^2.6.14然后npm install。验证方式npm list vue应显示└── vue2.6.14。5.5 数据库中文乱码餐厅名显示为问号这是MySQL字符集配置问题。执行以下SQL修复ALTER DATABASE springbooti23b2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE tb_restaurant CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER TABLE tb_dish CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后重启MySQL服务。6. 二次开发与功能扩展建议让项目真正为你所用6.1 快速接入微信支付三步替换支付模块原项目只支持余额支付要接入微信支付只需改三处1.后端在OrderService.createOrder()里把payType 1余额支付改为payType 2微信支付并调用微信统一下单APIhttps://api.mch.weixin.qq.com/pay/unifiedorder返回prepay_id2.前端在OrderDetail.vue里支付按钮点击后调用/api/order/wxpay?orderIdxxx拿到prepay_id后用WeixinJSBridge唤起微信支付3.回调新增WxPayController.notify()处理微信支付结果通知更新tb_order.pay_status为2已支付并触发订单状态推送。关键代码片段// WxPayService.java public MapString, String unifiedOrder(String orderId) { // 构造微信支付请求参数 MapString, String params new HashMap(); params.put(appid, wx1234567890); params.put(mch_id, 1234567890); params.put(nonce_str, UUID.randomUUID().toString().replace(-, )); params.put(body, 外卖订单); params.put(out_trade_no, orderId); params.put(total_fee, 100); // 单位分 params.put(spbill_create_ip, 127.0.0.1); params.put(notify_url, https://yourdomain.com/api/wxpay/notify); params.put(trade_type, JSAPI); // 签名并发送请求... return result; }6.2 添加短信验证码集成阿里云短信服务替换AuthController.sendCode()方法1. 引入阿里云SDKdependencygroupIdcom.aliyun/groupIdartifactIdaliyun-java-sdk-dysmsapi/artifactIdversion2.1.0/version/dependency2. 在application.yml里配置aliyun.sms.accessKeyId和accessKeySecret3. 发送逻辑生成6位随机码存入Rediskey为sms:code:${phone}过期5分钟调用IAcsClient.getAcsResponse(request)发送短信。6.3 性能优化建议从数据库到前端的全链路提速数据库层为tb_order表的user_id和create_time字段添加联合索引idx_user_time加速用户历史订单查询后端层在RestaurantController.listNearby()里用Redis缓存附近餐厅列表key为restaurant:nearby:${lat}:${lng}过期时间30分钟避免重复计算地理距离前端层在RestaurantList.vue里用v-infinite-scroll替代滚动加载配合IntersectionObserverAPI比scroll事件更高效部署层用Nginx反向代理前端静态资源开启gzip压缩把location /指向/dist目录location /api代理到http://localhost:8080。6.3 安全加固清单生产环境必须做的五件事Token密钥把rsa_private_key.pem和rsa_public_key.pem从resources目录移出放到服务器独立目录JwtConfig里用绝对路径读取数据库密码application.yml里的MySQL密码用Jasypt加密启动时传--jasypt.encryptor.passwordyour-secretSwagger关闭生产环境application-prod.yml里设springfox.documentation.enabledfalseSQL注入防护MyBatis所有查询用#{}而非${}动态SQL用bind标签预处理XSS过滤在WebMvcConfigurer里添加XssFilter对所有请求参数做HTML标签过滤。我在实际部署时把这五件事做完项目通过了第三方安全扫描SQL注入和XSS漏洞评分从高危降为低危。真正的“开箱即用”不是解压就能跑而是解压后按这份清单操作就能达到生产可用标准。本文还有配套的精品资源点击获取简介直接可用的外卖订餐系统全套开发资源后端用SpringBoot搭建整合MyBatis做数据访问、SpringMVC处理请求支持MySQL 5.7和8.0前端基于Vue.js Element UI实现适配PC与平板的响应式界面所有接口通过Token验证登录状态保障基础会话安全。项目采用Maven3构建兼容IntelliJ IDEA和EclipseWindows 10/11环境开箱即跑。压缩包里包含全部源码src目录、可执行数据库脚本springbooti23b2.sql、建库说明文档、前后端启动配置、mvnw启动工具及target编译输出目录。部署只需装好JDK 1.8、MySQL服务和IDE按文档导入项目、执行SQL初始化表结构、分别启动后端服务和前端工程就能体验完整业务流程手机号注册登录、查看附近餐厅、浏览分类菜品、加入购物车、提交订单、查看历史订单与订单状态更新。所有功能模块代码结构清晰注释完整适合学习SpringBootVue分离开发模式、理解Token鉴权落地方式也适用于二次开发或教学演示。本文还有配套的精品资源点击获取