拓冰建站拓冰建站
首页 / 资讯中心 / 正文

SpringBoot+Vue3+uni-app外卖App全栈开发:从数据库设计到订单状态机与打包上线

做外卖订餐App这个项目名一出来很多人的第一反应是“又要写一套用户下单、商家接单的后台”。但真正上手之后才会发现难点根本不在一张订单表怎么写而在于技术栈怎么选、角色权限怎么分、订单状态怎么流转、App端和后端怎么顺畅对上话。我这些年接过不少类似的全栈项目也帮人评审过一些半成品代码今天就把这个题目完整拆一遍从技术选型到数据库设计再到接口实现和App打包把能直接抄作业的部分都写出来。这篇内容适合准备做毕设、想练手全栈项目、或者打算真做一个轻量外卖平台的开发者参考。先回答一个绕不开的问题标题里堆了PHP、ASP.NET、SpringBoot、SSM、Vue3这么多技术到底是要全用还是选一个答案显然是后者。这题本质上是“多选一”的外卖系统实现只不过不同学校、不同团队的技术偏好不一样。我的建议很明确主路线用 Java SpringBoot Vue3配 uni-app 打包成AppSSM 作为理解 SpringBoot 底层的基础PHP 和 ASP.NET 在对比方案里讲清楚适用场景。下面按完整项目的落地顺序来拆。1. 项目全貌与技术选型思路1.1 标题里那堆技术栈到底是什么关系先把这六样东西摆正位置。SpringBoot 和 SSM 不是竞争关系SSM 是 Spring SpringMVC MyBatis 三个框架的组合而 SpringBoot 是对 Spring 体系的进一步封装让 SSM 繁琐的 XML 配置变成自动装配。换句话说你用 SpringBoot 写接口的时候底层还是 SpringMVC 那套请求路由机制还是 MyBatis或 MyBatis-Plus在做数据库映射。所以选了 SpringBoot并不等于抛弃 SSM而是站在 SSM 的肩膀上少写配置。PHP 和 ASP.NET 则是另外两条独立路线。PHP 比较适合传统的快速开发LNMP 环境一套就齐开发效率高但如果是做 App 的后端接口PHP 的生态相对分散要自己拼鉴权、拼参数校验维护成本不低。ASP.NET现代一点的用法是 .NET 6/8性能确实很强尤其适合跑在 Windows 服务器上的企业系统但招人、找资料、部署在 Linux 服务器的体验都不如 Java 生态顺畅。如果这是个人项目或毕设选 Java SpringBoot 最大的优势不是它性能最强而是遇到问题随手一搜就有答案踩坑成本最低。做个横向对比方便你直接抄结论技术栈学习曲线生态与资料部署难度适合场景我的建议Java SpringBoot中等非常丰富低中小型系统、毕设、企业实习首选主力SSM中高丰富中理解 Spring 底层的教学项目先学它再学 BootPHPThinkPHP/Laravel低多但杂低快速上线、个人站长备选不推荐新手做复杂业务ASP.NET中海外资料多中Windows 服务器环境公司强制才考虑1.2 App端不是套个网页壳跨端方案要提前定题目说要做一个“App”这是一个很容易被忽略的难点。很多人以为把 Vue3 写好的页面丢进 WebView 就算 App结果真机一跑卡顿、样式错位、相机和定位调不起来全是坑。外卖App必须要能选地址、上传头像、接收订单提醒这就意味着你得决定 App 壳用什么方案。我的建议是 Vue3 uni-app。uni-app 是一套代码同时编译到 iOS、Android、H5 和小程序的跨端框架它的语法基于 Vue组件模型也贴近小程序最关键的是它有成熟的原生插件市场调定位、调地图、调相机这类需求不用自己写原生代码。相比 React Native 和 Flutteruni-app 的学习成本低得多对“以后端为主、顺便把前端做完”的人来说最友好。如果你本身前端基础好也可以考虑 Flutter但那样工程量和上线成本都会上去。这里要划一个重点App 端和后端是分离的。后端只提供 JSON 接口App 端负责展示和交互。所以在后端的架构设计里一定要把这个前提想清楚接口返回什么字段、状态码怎么定义、权限怎么验证都要站在“给第三方客户端调用”的角度来设计而不是像传统网页项目那样对着页面写接口。1.3 后端技术选型的三条主线对比后端这里再多说几句。我的最终推荐是 SpringBoot 3.x MyBatis-Plus MySQL Redis可选。SpringBoot 3.x 基于 JDK17性能和生态都更现代。如果开发机还是 JDK8那就退一步用 SpringBoot 2.7.x功能上对这个项目完全够用而且能避开很多依赖兼容问题。热搜词里的“springboot版本太高”说的就是这个问题SpringBoot 3 以后 javax 包名全部改成 jakarta网上老代码直接复制大概率编译不过新手很容易在这里卡一两个小时。Redis 在这个项目里不是必须的但加了会明显提升体验。比如菜品分类缓存、购物车数据、短信验证码存储用 Redis 比用数据库更合适。如果环境不好搭第一步可以省掉订单、用户这些核心数据先放 MySQL 稳定跑通后面再补缓存。另一个需要强调的点是不管最后选 Java 还是 PHP、ASP.NET接口设计的思路是通用的。我见过很多新手把所有逻辑堆在 Controller 里一个下单接口写两百行后面想加个“满减活动”就得把整个方法重写。正确的做法是分层Controller 只做参数接收和路由分发Service 做业务逻辑MapperDAO做数据库操作。这段代码质量上的差异在订单这种状态多的业务里会体现得特别明显。2. 系统设计与数据库模型2.1 四个角色的功能边界外卖系统最少要拆成四个端用户端、商家端、骑手端、管理后台。很多人只做前两个结果评审的时候被问“订单没人配送怎么办”回答不上来。我的建议是骑手端可以简化但必须在设计里出现哪怕只有一个“接单”和“送达”按钮整个业务的闭环才算完整。用户端的功能包括注册登录、浏览商家和菜品、加入购物车、提交订单、在线支付可用模拟支付、查看订单状态、订单评价。商家端功能包括店铺和菜品管理、接单/出餐操作、查看营业数据。骑手端功能最简化为抢单/接单、标记取餐、标记送达。管理后台则负责审核商家、用户管理、数据统计。这四个端共用一套后端接口通过角色字段区分权限。权限这块最容易出漏洞。我见过很多项目只在页面层判断了“你是不是商家”App 接口却可以直接修改订单状态这就等于大门没锁。后端的每个敏感接口都要做二次权限校验用户只能操作自己的订单商家只能操作自己店铺的订单骑手只能操作自己接到的订单。这些校验代码很啰嗦但它是这个项目的评分亮点也是真实上线的基本要求。2.2 核心表设计与订单状态机数据库不会很复杂核心表大概八张用户表、商家表、菜品表、分类表、购物车表、订单表、订单明细表、骑手表可直接并入角色表。额外还可以加配送地址表、评价表。设计订单表的时候几个关键时刻的字段必须留好订单状态、下单时间、支付时间、接单时间、送达时间、支付方式、订单金额、配送地址快照。餐饮业务里最见设计功力的就是订单状态机。一句话总结状态流转必须是单向推进的不能出现“已送达还能改成已取消”这种倒回。我的推荐状态设计如下状态值含义可操作角色下一个状态0待支付用户用户支付或超时取消1已支付/待接单系统自动商家接单或超时退款2已接单/备餐中商家骑手取餐3配送中骑手用户确认或骑手标记送达4已完成-终态5已取消-终态6退款中用户/系统退款完成后回到已取消状态字段不建议直接用字符串“待支付”“配送中”建议用数字枚举代码里定义常量或枚举类数据库中只存数字。好处有两个数据库体积小一点是次要的主要是代码里判断状态时不容易写错也方便以后扩展新状态。2.3 数据库设计避坑清单第一订单金额一定要用 decimal(10,2)不要用 float。二进制浮点数在计算金额时会出精度问题比如 0.1 加 0.2 不等于 0.3这在支付场景里是绝对不能接受的。MyBatis-Plus 里对应 BigDecimal数据库用 DECIMAL 保存。第二删除不要用物理删除。用户下单记录、订单明细这种核心业务数据一定要用逻辑删除也就是加一个 deleted 字段查数据的时候默认过滤掉。原因很简单真实业务数据是要留着做统计和对账的真删了以后想复盘都找不回来。第三凡是作为查询条件的字段都要建索引比如 order 表的 user_id、shop_id、status这是后端接口性能的基本保障。举个例子一张表用户表大概长这样CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT, phone varchar(20) NOT NULL COMMENT 手机号, password varchar(100) NOT NULL COMMENT BCrypt加密后的密码, nickname varchar(50) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role tinyint DEFAULT 1 COMMENT 0:管理员 1:普通用户 2:商家 3:骑手, status tinyint DEFAULT 1 COMMENT 1:正常 0:禁用, created_at datetime DEFAULT NULL, updated_at datetime DEFAULT NULL, deleted tinyint DEFAULT 0, PRIMARY KEY (id), KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单表同理也要把状态机字段和关键时间字段全部放进去创建一个idx_user_id和idx_shop_id。这些基础打好了后面写接口会非常顺手。3. 后端接口与核心流程实现3.1 工程结构与统一返回体有了数据库设计接下来就是搭后端工程。我习惯的目录结构是这样的controller、service、mapper、entity、dto、vo、common、config。entity 对应数据库表dto 是接收前端参数的vo 是返回给前端的数据结构common 放统一返回体、异常处理、常量等。把 dto 和 vo 分开是一个很容易被新手忽略但很重要的细节因为前端传过来的参数和后端返回给前端的数据结构往往不一样硬用一个对象去承接两边代码会越写越乱。统一返回体是这个项目里必须有的东西。如果每个接口返回格式都不一样App 端解析数据会非常痛苦。我通常定义一个 Result 类public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT error(Integer code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; } }配套的还要有一个全局异常处理器用RestControllerAdvice把业务异常统一捕获并转换成上面的 JSON 格式。这样 Controller 里就不用到处写 try-catch代码清爽很多。见过太多项目一个空指针异常直接把整个堆栈丢给前端这在联调阶段折磨死人也会暴露内部实现细节。3.2 注册登录与JWT鉴权用户鉴权我用 JWT而不是传统 Session。原因是 App 端没有 Cookie 概念跨端请求也不适合维护 Session。JWT 的逻辑是用户登录成功后后端签发一个带过期时间的 token 返回给前端前端每次请求在请求头里带上Authorization: Bearer token后端用一个拦截器解析 token拿到用户 id 和角色。SpringBoot 3 里实现一个拦截器并不复杂核心思路是注册一个 HandlerInterceptor在 preHandle 里校验 token。校验通过就把用户信息放进 ThreadLocal 或请求上下文后续接口直接取。这里要注意两个点一是密码存储一定要用 BCrypt 加密不要用 MD5。MD5 不加盐很容易被彩虹表撞库项目里别省这一步。二是 JWT 的密钥要放在配置文件里不要硬编码到代码中防止代码提交到 Git 后被泄露。JWT 工具类的核心代码可以这样写public String generateToken(Long userId, Integer role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() 7 * 24 * 60 * 60 * 1000)) .signWith(secretKey) .compact(); }过期时间我习惯设成 7 天对内部项目足够。如果做真实的支付类产品建议把 token 时间缩短到 2 小时配合 refresh_token 防止用户频繁登录但那套机制复杂度比较高毕设阶段可以不用做。3.3 下单到配送的核心接口流程下单是整个系统里最需要想清楚的接口。一个完整下单接口的流程是校验用户登录和购物车数据检查商家营业状态计算订单总金额包括菜品价格、配送费、满减优惠生成订单主表和订单明细表清空购物车然后返回订单号让前端去支付。如果订单金额算错那是要出大问题的。有的项目把“下单”和“支付”合在一起做这不对。真实场景里用户可能下单后不想付了或者支付超时所以这两个动作必须拆开。我建议的接口设计如下PostMapping(/api/order/create) public ResultLong createOrder(RequestBody CreateOrderDTO dto) { Long orderId orderService.createOrder(userId, dto); return Result.success(orderId); } PostMapping(/api/order/pay) public ResultString pay(RequestParam Long orderId) { orderService.pay(orderId); return Result.success(支付成功); }支付接口在真实项目里要对接微信支付或支付宝这需要企业资质和证书。毕设或者演示项目一般用“模拟支付”代替也就是用户点支付按钮后直接调用本接口把订单状态从“待支付”改成“已支付”在后端代码里注释清楚“此处对接真实支付平台”。千万不要真的在代码里写死“支付成功”的假数据而是要走完状态流转这样评审演示的时候逻辑才站得住脚。下单之后商家端接口围绕订单状态流转来写商家查待接单列表、商家接单、商家出餐骑手端查可接单列表、骑手接单、骑手标记取餐、骑手标记送达。这些操作本质都是更新订单状态但必须在 Service 层校验“当前状态是否允许流转到目标状态”。我一般会写一个checkOrderStatus(order, expectedStatus)的工具方法所有状态变更统一走它校验能把很多脏数据问题提前拦掉。3.4 超时订单与并发控制外卖系统必然要考虑“用户下单后不支付”的情况。真实平台的做法是用消息队列延时任务或者定时扫描数据库把超过 15 分钟未支付的订单自动取消。对于个人项目直接写一个 Spring 定时任务就行Component public class OrderTimeoutTask { Scheduled(cron 0 */5 * * * ?) public void cancelExpiredOrders() { // 查询创建时间超过15分钟且状态为待支付的订单 // 批量更新为已取消并恢复库存如果有库存概念 } }这里要注意 cron 表达式的含义每隔 5 分钟扫描一次比实时性要求更高的场景可以把频率调高但也要考虑数据库压力。如果用了 Redis可以把“待支付订单id 过期时间”存在 Redis 里利用过期回调实现准实时处理效果更好。并发控制这块最关键的是“防止重复下单”和“防止库存超卖”。简单项目可以在用户表加一个下单锁或者在创建订单前先查询是否已有相同内容的未支付订单有则提示去支付更严谨的做法是 Redis 分布式锁但说实话一个课程设计项目不至于上到那一步。只是要给读者提个醒在接口层做好幂等判断别让用户狂点提交按钮生成几十个重复订单。4. 前端与移动端落地4.1 Vue3 uni-app 的项目初始化前端这边管理后台用 Vue3 Element Plus用户端、商家端、骑手端这三个移动端用 uni-app 打包成 App。这样划分比较合理后台是 PC 网页移动端是 App两者共用后端接口但界面完全不同。uni-app 的创建有很多种方式我推荐用 vue3 vite 模板别用 vue2 版本毕竟已经是过时技术了。创建命令很简单npx degit dcloudio/uni-preset-vue#vite my-takeout-app cd my-takeout-app npm install npm run dev:app这里会遇到一个很经典的问题node 版本。uni-app 的构建依赖 vite而新版本 vite 对 node 版本有要求建议 node 16 以上最好用 nvm 管理 node 版本。如果之前已经装了 vue2 的老项目别忘了把 node_modules 删掉重新 install经常有人卡在“为什么 npm run dev 报一堆错”其实就是版本缓存没清干净。4.2 请求封装与登录态同步移动端要封装统一的请求工具不能每个页面直接拿 uni.request 写。我习惯封装一个小模块统一处理 baseURL、token 注入、错误提示、401 跳登录。代码可以写成这样const BASE_URL http://localhost:8080; export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { uni.navigateTo({ url: /pages/login/login }); reject(res.data); } else { uni.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: (err) { uni.showToast({ title: 网络请求失败, icon: none }); reject(err); } }); }); }登录态同步是个容易被忽略的细节。App 启动时应该检查本地有没有 token有 token 就调一次/api/user/info刷新用户信息没有就直接跳登录页。每次请求如果后端返回 401说明 token 过期要清掉本地登录态回登录页重新登录。这些机制做扎实了App 用户体感才会正常不会出现那种“数据加载中但实际 401 报错”的怪现象。4.3 三端复用与页面规划移动端三端虽然角色不同但可以共用一个工程用 tabbar 和路由权限来区分。用户端的 tabbar 是“首页、订单、我的”商家端的 tabbar 是“订单管理、菜品管理、我的店铺”骑手端则是“接单大厅、我的配送”。我建议在工程的pages.json里配好所有页面登录后根据角色分发到对应入口。权限校验在前端只做展示层控制真正能做主的是后端接口。首页一般需要定位到当前城市、获取附近商家列表、按分类筛选菜品。商家详情页则是店铺介绍、菜品列表、加购物车。购物车页面是最容易写乱的部分因为要处理不同商家的商品不能合并的问题。我的办法是购物车数据结构以 shopId 为维度分组后端结算的时候按商家拆成多个订单或者直接把“跨店结算”限制掉提示用户“当前仅支持单店结算”这样很多业务逻辑都会简单很多。4.4 真机调试与打包注意事项开发阶段在浏览器里打开 H5 页面调试是很舒服的但真正测试相机、定位、推送这些能力还是得真机跑。uni-app 用 HBuilderX 连接手机调试很方便但要注意真机访问本地后端接口时不能写localhost必须写电脑在局域网上的 IP。比如电脑 IP 是192.168.1.100后端接口地址就是这个 IP 加端口。如果安卓手机依然访问不通检查一下电脑防火墙是否放行了 8080 端口。打包成 App 的步骤在 HBuilderX 里是“发行-原生App-云打包”第一次打包需要注册 DCloud 账号云打包就是拿他们的服务器帮你编译成 apk。这里有个坑云打包默认的包名和证书是测试的如果要上架应用商店必须自己申请包名和签名证书。如果是个人项目演示测试包完全够用直接勾选“使用公共测试证书”即可。注意不要选“广告“, 也别勾选额外模块除非你确实用到了否则打包时间会拉长而且包体变大。5. 常见问题与排查技巧实录5.1 跨域问题排查前端开发最常遇到的就是跨域。后端接口地址是http://localhost:8080前端页面跑在http://localhost:5173浏览器默认会拦截跨域请求。解决办法是后端加一个 CORS 配置类或者用CrossOrigin注解。SpringBoot 的全局跨域配置很简单Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意addAllowedOriginPattern(*)和setAllowCredentials(true)要一起用。如果allowCredentials设置为 true就不能简单用addAllowedOrigin(*)必须用 pattern 形式否则部分版本的 Spring 会直接报错。这个点很细但搜一遍“SpringBoot 跨域 配置了还是不行”就会发现踩的人特别多。如果配了跨域仍然失败优先清浏览器缓存换无痕窗口试试。5.2 图片上传与访问路径外卖系统里商家要传菜品图片用户要传头像。图片最简便的存储方案是存到服务器本机某个目录数据库只保存相对路径比如/upload/2025/02/xxx.png。后端提供一个上传接口用 MultipartFile 接收文件写到磁盘。再配一个静态资源映射让/upload/**直接映射到磁盘目录。这样二维码、菜品图都能通过 URL 直接访问。但要注意两点第一上传目录不要放在项目源码目录下否则打包发布时会被格式化掉要放到服务器上一个独立目录。第二文件名不能直接用原始文件名会有重名覆盖和路径穿越风险建议用 UUID 或时间戳重命名。图片压缩这个需求在热词里也出现了真做商用系统时用户传一张 5MB 的照片会拖慢列表加载后端可以用 Thumbnailator 之类工具压缩毕设阶段不是必须。5.3 时间与金额的精度问题前端传时间建议统一用字符串格式yyyy-MM-dd HH:mm:ss后端用 LocalDateTime 接收避免因为时区不同导致时间错乱。数据库里时间字段用datetimeJava 里对应LocalDateTime。JSON 序列化要配置一下SpringBoot 默认会把 LocalDateTime 序列化成数组前端拿到很奇怪。在配置文件里加一句spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8金额问题我已经强调过了一定用 BigDecimal数据库用 DECIMAL。前端展示金额也尽量用“分”为单位做计算避免浮点误差。后端接口返回的金额字段用字符串格式的28.50不要返回数字28.5防止前端 toFixed 坑掉精度。5.4 安全细节速查整理一个常见问题速查表方便开发时自查问题现象可能原因解决办法所有接口返回 401token 没放请求头或后端拦截器没放行登录接口检查请求封装确认登录接口在拦截器白名单前端能访问列表提交订单却报错参数名对不上或 DTO 里缺少必填字段打开浏览器 network对比请求参数和后端 DTO 字段密码明文存数据库注册接口把 password 直接入库改用 BCrypt 加密BCrypt.checkpw 校验商家能取消别人的订单订单方法里没校验商家归属Service 层查询订单后先判断 order.getShopId() 是否属于当前商家打包成 App 后图片加载不出来图片 URL 写的是 localhost把图片访问路径改成局域网 IP 或线上域名这些坑几乎每一个项目都会踩一遍尤其“商家操作他人订单”这种权限漏洞哪怕前端页面上没露出来直接调接口也能攻击。做的时候多花十分钟把这些场景补上项目质量会明显上一个台阶。6. 项目还能怎么继续进化最后从实际项目经验角度聊一点心得体会。把“下单、支付、接单、配送、完成”这条主链路跑通这个项目已经完成大半了。但如果想让它在答辩或简历上更有竞争力可以再加三个方向一是订单实时推送用户下单后商家端页面能实时收到新订单提醒这个可以基于 WebSocket 实现前后端各加一点代码二是引入 Redis 做菜品缓存和购物车把热点数据的访问压力降到最低三是加一个简单的数据看板商家后台展示今日订单数、营业额、热门菜品本质就是一个聚合统计 SQL但视觉冲击力很强。我自己实际做过不少类似系统最大的感受是这类项目的复杂度和成就感都藏在细节里。很多人一开始觉得外卖系统就是 CRUD做着做着才发现订单状态流转依赖设计、权限校验不能省、金额精度不能马虎、App 打包链路长。把这些细节一个个解决掉水平提升比刷几十道面试题都明显。如果文章里的某个方案和你的实际环境有出入以你本地的依赖版本为准尤其是 SpringBoot 3 和 2 的差异遇到编译报错先看看是不是包名和版本的问题。这一套流程完整走一遍之后你就能从“会写接口”进化到“能单独负责一个全栈项目”了。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门