“苍穹外卖 01”这个标题,在Java后端的学习圈里几乎算得上一个“暗号”了。很多刚走完SSM或Spring Boot入门教程的人,都会在某个阶段遇到这个名字。它不是一个真实上线的商业项目,而是一个高度贴近真实业务场景的实战练习项目:核心业务是一套外卖平台的完整后端服务,覆盖管理端、客户端和骑手端三块最常见的业务域。01作为系列的第一篇,承担的任务通常是需求梳理、技术选型、环境搭建和基础模块落地,也就是把一个项目从“零”真正推到“能跑起来、能登录”的阶段。
这篇文章就是围绕这个第一阶段来写的。里面会把这套项目的整体设计思路拆开,把数据库建模、登录鉴权、员工管理这些核心模块的实现细节讲清楚,最后再把我自己实操中踩过的坑和排错顺序一并整理出来。适合两类人看:一类是刚学完框架基础、准备用完整项目练手的人,另一类是已经做到中间阶段、想回头把地基部分补扎实的人。
1. 项目整体设计与需求拆解
1.1 外卖业务到底在做什么事
先想清楚业务,再碰代码。外卖平台表面看就是“用户下单、商家出餐、骑手配送”,但真正落地成系统,里面的角色关系比想象中复杂。
苍穹外卖的业务模型大致围绕三个角色展开。管理端是平台运营方使用的,负责管理商家入驻、菜品上下架、分类调整、订单监控和营业数据统计;客户端是普通用户使用的,负责浏览分类、挑选菜品、加入购物车、提交订单、在线支付和查看订单状态;骑手端则是接单、取货、配送、完成订单的流转变现。
01阶段主要聚焦在管理端的基础能力上。后台登录、员工管理、分类管理、菜品管理,这几个模块是后续所有业务的地基。没有一套可靠的员工账号体系和权限边界,后面无论做订单流还是数据统计,都会失去身份依据和操作入口。
1.2 核心功能模块的优先级排序
新手拿到这种项目,最容易犯的毛病是“全都要”。我见过有人一上来就规划十几个模块,数据库建了二十多张表,结果做了两个月还在调登录接口。我的建议是:第一阶段的优先级一定是“管理端可登录、可维护基础数据”。
按这个思路,01阶段要落地的模块应该圈定在四个方面。
员工登录与鉴权是最优先的一环,没有它,后续所有接口都无法做身份校验。其次是员工信息管理,包括查询员工列表、新增员工、启用禁用账号、修改密码这类日常运维操作。然后是分类管理,外卖平台的菜品分类支持一级和多级结构,这是菜品模块的前置依赖。最后是菜品与套餐的基础管理,包括文件上传、菜品的新增和分页查询。
至于购物车、订单、支付、配送这些高业务复杂度的模块,全部放到后面的阶段性规划里。这样安排的逻辑很简单:每个阶段能“完整闭环”地上线一部分可演示的功能,而不是做一个永远调试不完的半成品。
1.3 标题里的“01”在整条学习线里的定位
如果把这套项目比作盖楼,01阶段就是“打地基”和“出正负零”的过程。它不做复杂的业务算法,不碰高并发的订单峰值问题,更不意味着把极简的框架Demo再抄一遍。它的真正价值在于:让你用接近工业级的方式,把一套前后端分离的看板结构搭起来,并且跑通一个带鉴权后台的最小业务闭环。
一个合格的01阶段,至少应该达到三个验收标准:第一,管理后台能通过账号密码正常登录,未登录状态访问业务接口会被拦截;第二,员工数据能通过后台进行增删改查,密码不是明文存储;第三,数据库里的表结构不是拍脑袋设计的,能支撑后续菜品、分类、订单等业务扩展。
2. 技术选型与方案设计思路
2.1 后端主框架为什么用 Spring Boot 搭配 MyBatis
苍穹外卖的01阶段,技术栈选型非常“经典”:Spring Boot作为应用主框架,MyBatis作为持久层框架,MySQL作为主数据库,Redis负责缓存与部分会话数据,前端部分则通过小程序或管理后台Vue项目对接,前后端通过JSON格式的RESTful接口交互。
这个组合在当前环境下不是“最新潮”的,但一定是“最适合教学和练手”的。Spring Boot解决的是传统Spring配置繁琐的问题,它用自动配置把大量的基础设施工作压缩成几个注解和依赖坐标。MyBatis相比JPA和MyBatis-Plus,保留了手写SQL的控制力,这一点在教学阶段特别重要:你能通过每一行SQL真实感知数据库交互的过程,而不是被ORM框架过度封装。
2.2 缓存、存储与工具组件怎么分工
外卖系统天然是“读多写少”的业务模型。用户打开App最先看到的分类、菜品列表,这些数据在一天之内变化频率极低,但访问次数极高。这种场景非常适合引入Redis做缓存。01阶段虽然还没有完全展开客户端的高频读接口,但缓存组件应该从一开始就预置进来,否则后面再接入会觉得代码结构很别扭。
文件上传也是外卖系统的刚需。菜品图片、营业执照、身份证照片,都需要一个可靠的对象存储通道。在本地开发阶段,最务实的做法是搭建一个本地文件存储服务,实现文件上传后返回可访问的URL。这样既不需要依赖第三方云厂商,又能让你理解文件上传的完整链路。等部署到服务器后,再替换成云存储的SDK实现。
2.3 前后端分离的项目结构怎么组织
工程结构上,这套项目采用前后端分离。前端的管理后台用Vue3或微信小程序实现,后端统一提供/admin、/user、/common等前缀的RESTful接口。实际开发中,前端代码和后端代码一定要分成两个独立工程目录,不要混在一个仓库里。后端内部则按功能模块分包,而不是按技术类型分包。
举个例子,controller、service、mapper这种按层分包的方式,在小项目里看着挺清爽,一旦功能模块变多,包之间的依赖会越来越乱。更推荐的做法是:先按业务域拆分成employee、category、dish、setmeal,在每个业务域内部再组织controller、service、mapper子包。这样从包结构上就能看出系统有哪些业务模块,而不是靠翻目录猜。
3. 环境准备与核心数据模型设计
3.1 开发环境的版本搭配建议
01阶段的环境搭建卡住很多人,往往不是能力问题,而是版本搭配问题。Java、Maven、MySQL、Redis这几个组件,版本不兼容会引发一堆让人抓狂的报错。我自己常用的组合是:JDK 1.8或11、Maven 3.6.x以上、MySQL 8.0、Redis 5.x或6.x、前端管理后台用Node 16或18。
重点提醒一句:如果本机已经装了MySQL 5.x,建议优先切换成8.0再开始这个项目。虽然5.7也能跑,但密码认证插件变化会导致驱动连接配置出现差异。Spring Boot 2.7.x版本搭配mysql-connector-j8.0.x时,driver-class-name是com.mysql.cj.jdbc.Driver,连接串里别忘了加useSSL=false&serverTimezone=Asia/Shanghai这两个参数,否则时区或SSL握手会报错。
3.2 5张核心表的业务血缘关系
01阶段的数据模型不需要一次性铺开全量表,但关键的几张核心表必须在这个阶段里设计好。它们是后续扩展的地基。
第一张是员工表employee,主键用id,核心字段包括username、password、name、phone、status,外加create_time、update_time、create_user、update_user这类审计字段。外卖系统的管理员账号体系相对简单,但密码字段绝不能用明文。
第二张是分类表category,核心字段包括name、type、sort、status。这里的type用来区分分类是菜品分类还是套餐分类,取值为1或2。sort用于控制同类型分类在页面上的展示顺序,数字越小越靠前。
第三张是菜品表dish,核心字段包括category_id、name、price、image、description、status。菜品对外卖系统来说是核心商品,category_id通过外键逻辑关联到分类表。第四张是口味表dish_flavor,它和菜品表是多对一的关系,专门存放“辣度”“规格”之类的标签型数据。
第五张是套餐表setmeal,以及配套的套餐菜品关系表setmeal_dish。套餐本质上是菜品的组合,所以setmeal_dish里既要有套餐ID,又要有菜品ID和份数,才能把多对多关系拆成两张表存储。
3.3 建表SQL里的关键约束怎么写
实际写建表语句时,有四个细节值得特别强调。
第一,主键统一用bigint类型并配合自动递增,不推荐业务主键,尤其不推荐用手机号或用户名当主键,一旦账号需要修改标识,主键变动会引发连锁问题。第二,业务表强制加create_time、update_time两个时间字段,并让代码层统一维护,这样查问题的时候能追踪数据演变的时序。第三,status字段统一用tinyint,0表示禁用,1表示启用,命名上直接见名知意,不要用is_delete这种容易和逻辑删除混淆的词。第四,价格字段用decimal(10,2)而不是double,避免浮点精度造成金额偏差。
员工表的初始化数据尤其要注意。系统必须内置一个超级管理员的初始账号,否则后台连登录都进不去。常见做法是插入一条username为admin、密码为MD5加密后密文的记录。
4. 后台登录与员工管理模块实现
4.1 统一的响应结果与异常处理体系
后台接口如果直接返回裸数据,前后端联调时会出现一个很尴尬的问题:接口要么返回对象、要么返回字符串、要么返回布尔值,前端拿到什么都需要猜。工整的做法是先定义一套统一响应结果体,通常包含三个字段:code表示业务状态码,msg表示提示信息,data表示具体数据。
这个封装类通常命名为Result,提供静态方法success()和error()。业务中所有Controller的返回值都用这个类包一层。这样做的好处不只是规范,更重要的是后续可以统一做全局异常兜底:不管Service层抛出业务异常还是系统异常,全局异常处理器都能捕获,并返回统一格式的错误信息,避免白屏和堆栈信息直接暴露给前端。01阶段就很值得把这件事做扎实,后面你写的每个接口都会受益。
4.2 JWT令牌的生成与登录态校验
管理端是内部系统,但也不能裸奔。登录状态管理在01阶段选用JWT(JSON Web Token)是合理的。用户在后台输入账号密码,后端核验成功后生成一个JWT令牌,令牌里可以存放员工ID和用户名这类非敏感信息,作为后续接口的身份凭证。
JWT的核心机制是签名防篡改。实现时可以用io.jsonwebtoken的Jwts库,设置一个足够复杂的签名密钥,通过Jwts.builder()设置主题、签发时间、过期时间,再压入自定义的claims业务数据,最后生成一个紧凑型字符串。登录接口返回这个令牌,前端拿到后存到localStorage或请求头里,后续每次请求携带token字段。
权限控制最方便的做法是写一个拦截器。但这个拦截器有个特别容易踩的坑:一定要在拦截器里放行登录接口、静态资源等白名单路径。如果把/admin/employee/login也拦截了,用户连登录都提交不出去,就会出现“启动没问题,但一请求就401”的诡异状态。
4.3 密码加密与员工信息管理
外卖管理后台的密码存储不能是明文。就算这个项目是教学用途,也应该从一开始就建立正确的安全意识。常用的做法是MD5加盐或BCrypt加密。MD5加盐相对简单,使用门面类DigestUtils.md5DigestAsHex,把原始密码和固定盐值拼在一起再取摘要。这里要注意,盐值不能是随机值,否则旧账号密码没法做校验。如果后面想做更强的方案,使用BCrypt是更好的升级方向。
员工管理接口在01阶段至少要实现四个:分页查询员工列表,支持按姓名模糊搜索;新增员工,用户名为唯一标识,密码默认用初始密码加密后写入;启用禁用员工账号,前端通过修改status字段完成;编辑员工基本信息并更新数据库。这里有个实际经验是分页查询的返回格式,PageResult里要同时包含total总数和records列表数据,前端的分页组件才能正确渲染总页数。
4.4 为什么这个阶段要把登录代码写得格外细
很多新手在练登录功能的时候,觉得“能通就行”,代码写得非常随意。用户名密码直接查库比对、不走缓存、不设过期时间、异常统一吞掉。等做到后面订单模块,需要从登录令牌中拿用户身份时,才发现claims里没放用户ID,又回头改登录逻辑,还不敢轻易动线上数据。
所以我特别建议在01阶段就把登录链路按“生产级标准”写完整。包括密码加密验证、账号状态校验、JWT生成与过期策略、拦截器统一鉴权、全局异常处理。哪怕前端页面只有登录框和列表页,这个闭环也是完整的。后面再加新接口,只需要关注业务逻辑本身,不需要反复回来补基础设施。
5. 常见问题与排查技巧实录
5.1 应用启动失败,端口被占用
这个问题出现的频率极高。Spring Boot默认使用8080端口,如果本机有其他服务占用了8080,启动会报Port already in use错误。排查顺序很简单:先看报错信息,java.net.BindException后面通常会带着端口号。然后执行netstat -ano | findstr 8080(Windows)或lsof -i:8080(Mac/Linux)找到占用进程的PID,杀掉即可。
如果不想杀进程,也可以在application.yml里修改server.port,但这会带来连锁问题:前端开发环境的代理配置、后端自测的接口地址都要跟着改,所以尽量还是保持默认端口。
5.2 数据库驱动找不到或连接超时
这个报错有两个常见方向。第一个是Maven依赖没拉下来,pom.xml里写了mysql-connector-j依赖,但本地仓库里缺包,启动时出现ClassNotFoundException。解决办法是先执行mvn clean再执行mvn compile,让Maven重新下载依赖文件。第二个是连接串配置错误。spring.datasource.url必须指定useUnicode=true&characterEncoding=utf-8,否则中文数据写入后大概率出现乱码,查询统计也会错乱。
5.3 MyBatis查询结果为空,但SQL在数据库能查到
这是MyBatis新手最容易困惑的问题之一。数据库里明明有数据,接口返回却是空列表,或者在console里看不到SQL日志输出。先检查Mapper接口的包扫描配置,@MapperScan注解的basePackages必须指向Mapper接口所在的完整包路径。其次,检查application.yml里的mybatis.mapper-locations配置,如果映射文件放在resources/mapper下,但是xml文件的命名空间和接口全限定名不一致,就会出现这种“静默失败”。
更隐蔽的一个问题是,实体类的setter写错或者属性名和数据库字段对不上。MyBatis底层通过反射给属性赋值,如果数据库列名是createTime,而实体类属性是create_time,又没有开启驼峰映射,结果就是createTime一直是null。
5.4 接口返回404或路径匹配不上
Spring Boot的接口路径匹配规则比较严格。Controller类上的@RequestMapping("/admin/employee"),方法上的@PostMapping("/login"),实际请求路径就是/admin/employee/login。如果前端请求带了尾斜杠,或者Controller类上漏写了路径前缀,都会出现404。
排查技巧是打开日志里的请求映射列表。在application.yml里配置logging.level.org.springframework.web=debug,启动后会打印所有已注册的HandlerMapping路径。对照日志里实际注册的路径和前端调用的路径,哪里对不上,问题就浮出水面了。
5.5 Token拦截导致登录接口永远不通
拦截器配置的顺序问题,我前前后后在这个坑里摔过三次。如果拦截器注册时没有排除登录接口,会出现一个逻辑死锁:登录接口本身需要放行,拦截器却拦截了所有请求,于是前端调/login时直接被拦截,提示未登录,无法走到登录逻辑。
正确写法是在WebMvcConfigurer里配置拦截器时显式调用excludePathPatterns,把登录接口、静态资源路径、文件上传预览路径都加进去。这里还要注意路径匹配的写法,/admin/employee/login只匹配一个精确路径,如果想排除整个模块的登录相关接口,用/admin/employee/login/**通配会稳妥一些。
5.6 前端联调时跨域请求被拦截
前端开发服务器一般在localhost:8081或5173端口,后端接口在8080,浏览器会触发CORS跨域策略。解决办法是在后端添加一个WebMvcConfigurer的配置类,重写addCorsMappings方法,允许的源设为前端开发地址,允许的请求头包含token字段。这里容易遗漏的是设置前端自定义的请求头。如果只设置了allowedOrigins,前端请求带了Authorization或token头,还是会被预检请求拦下来。所以allowedHeaders和exposedHeaders都要配全。
结束语
“苍穹外卖 01”这个阶段,最难熬的往往不是代码本身,而是从“会用框架写接口”到“能独立设计整个后台系统”的思维转变。我自己刚开始练这个系列时,光是数据库表就推翻重来了一次——第一次建表没有审计字段,没有状态字段,做到员工禁用功能时发现缺字段,只能回头改表结构、改实体、改接口,牵一发动全身。所以我会特别强调:01阶段宁可多花两天把需求拆透、把表设计稳,也不要急着往下写功能。
再有就是调试习惯。一套项目做到中后期,大量时间其实花在排查问题上。我的习惯是日志一定从第一天就规范输出,异常一定要有堆栈和上下文,接口联调前先拿工具手测一遍。这些不起眼的习惯,比多刷一百道题都管用。如果你现在正好卡在哪里跑不通,先按第5节的顺序逐项检查,大概率能找到问题的根源。这个项目后续的内容还很多,分类、菜品、购物车、订单,每一个模块都是在上面的地基上往上盖的。把01阶段真正吃透,后面的路会顺很多。