苍穹外卖JavaWeb项目实战:环境搭建、核心模块与避坑指南 简介这是一套面向JavaWeb初学者与进阶开发者的「苍穹外卖」在线订餐系统完整项目资料适合希望通过真实项目巩固Servlet、JSP、MVC与框架集成等技能的学习者。压缩包共约2000个文件整体约13.61MB以388个xml配置、146个java源码、144个class字节码为主另含yml、iml、json及md文档等覆盖后端代码、构建配置与项目说明。项目围绕用户、菜品、订单等数据库表设计串联Servlet控制层、JSP视图层与Model业务逻辑并涉及Spring依赖注入、MyBatis持久化、文件上传下载、登录鉴权与SQL注入防御等知识点同时包含JUnit测试与Tomcat部署相关内容。目前已有16391人学习下载读者可借助完整源码与笔记梳理项目目录结构、理解各模块实现细节并对照文档完成环境搭建与功能调试积累可写入简历的实战经验。1. 从一份外卖项目笔记说起为什么我建议你亲手跑一遍很多同学学完 JavaWeb 之后简历上能写的项目翻来覆去就是那几套面试官一眼就能看出是跟着视频敲的。我手里这份「苍穹外卖」的完整笔记和代码是一个做外卖业务场景的 JavaWeb 综合项目覆盖了从环境搭建、后端接口开发、数据库设计到前后端联调的全流程。它适合已经学过 Java 基础、MySQL、Spring Boot 但缺少一个完整项目练手的人也适合想拿它当课程设计或简历项目的人。我前后花了一个月把它拆完踩过的坑比想象中多所以这篇笔记不打算只给你一份代码而是把「怎么跑起来、参数怎么配、哪里容易翻车」讲清楚你照着做能少走弯路。2. 环境与依赖把项目跑起来需要哪些前置条件2.1 技术栈拆解与选型理由这个项目是典型的 JavaWeb 单体架构后端用 Spring Boot 做基础框架持久层用 MyBatis 或 MyBatis-Plus数据库是 MySQL缓存用 Redis接口文档用 Swagger 或 Knife4j前端一般是 Vue 或直接静态页面。为什么用这套组合因为它是目前国内中小型项目最主流的搭配面试问到的概率最高而且每个组件都有大量现成资料遇到问题好查。具体来说Spring Boot 负责把 Tomcat 内嵌进去省去手动部署 war 包的麻烦MyBatis 负责把 Java 对象和数据库表字段映射起来写 SQL 灵活Redis 主要用来存 token 和缓存菜品、分类这些高频读取的数据减轻数据库压力。你如果只学过 SSM直接上 Spring Boot 会有点不适应但跑通一个项目之后回头看 SSM 反而更清楚。提示不要一上来就改代码先把原始项目完整跑通一遍确认环境没问题再动手改否则出了问题你分不清是环境问题还是代码问题。2.2 环境搭建的具体步骤第一步确认 JDK 版本。这个项目一般要求 JDK 8 或 JDK 11我建议用 JDK 8因为很多依赖包对高版本 JDK 兼容性不好。安装完在命令行执行java -version看到版本号输出就说明 JDK 装好了。如果提示找不到命令检查环境变量JAVA_HOME是否指向 JDK 安装目录Path里是否加了%JAVA_HOME%\bin。第二步安装 MySQL 和 Redis。MySQL 建议用 5.7 或 8.0Redis 用 Windows 版或 Docker 版都行。MySQL 安装时注意字符集选utf8mb4否则中文和特殊符号会乱码。Redis 默认端口 6379启动后执行redis-cli ping返回PONG就正常。第三步导入数据库脚本。项目一般会提供一个.sql文件里面包含建表语句和初始数据。用 Navicat 或命令行导入mysql -u root -p your_database init.sql这里your_database是你提前建好的数据库名init.sql是脚本路径。导入完成后用show tables;确认表都建好了。第四步修改配置文件。打开application.yml或application.properties把数据库连接、Redis 连接改成你自己的spring: datasource: url: jdbc:mysql://localhost:3306/your_database?useUnicodetruecharacterEncodingutf-8serverTimezoneAsia/Shanghai username: root password: your_password redis: host: localhost port: 6379 database: 0serverTimezone这个参数特别容易漏漏了会报时区错误导致启动失败。characterEncoding也要加上不然中文数据存进去变问号。第五步启动项目。在 IDEA 里找到主启动类右键运行。控制台看到Started Application in x seconds就说明成功了。如果报端口占用改server.port换个端口如果报数据库连接失败回头检查用户名密码和数据库名。2.3 前后端联调的关键配置前端如果是 Vue 项目需要改vue.config.js里的代理配置把/api开头的请求转发到后端端口devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } }changeOrigin设为true是为了让后端收到的请求头里的 host 是目标地址避免跨域拦截。pathRewrite是把/api前缀去掉因为后端接口本身不带这个前缀。这两个参数配错前端就会报 404 或跨域错误。3. 核心模块实现从登录鉴权到订单流程3.1 登录鉴权与 JWT 令牌外卖项目绕不开登录因为不同角色管理员、用户看到的菜单和能操作的接口不一样。常见做法是用 JWT 做无状态鉴权用户登录成功后后端生成一个 token 返回给前端前端每次请求都在请求头里带上这个 token后端拦截器校验 token 是否有效。生成 token 的核心代码大概是这样public String createToken(Long userId, String secret, long ttlMillis) { // 设置签名算法和密钥 SignatureAlgorithm signatureAlgorithm SignatureAlgorithm.HS256; long nowMillis System.currentTimeMillis(); Date now new Date(nowMillis); // 用密钥生成签名 byte[] apiKeySecretBytes DatatypeConverter.parseBase64Binary(secret); Key signingKey new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName()); // 设置 JWT 的 payload JwtBuilder builder Jwts.builder() .setId(String.valueOf(userId)) .setIssuedAt(now) .signWith(signatureAlgorithm, signingKey); // 设置过期时间 if (ttlMillis 0) { long expMillis nowMillis ttlMillis; builder.setExpiration(new Date(expMillis)); } return builder.compact(); }secret是服务端保存的密钥不能泄露给前端ttlMillis是过期时间一般设 30 分钟到 2 小时。拦截器里解析 token 时如果过期会抛ExpiredJwtException你要捕获这个异常并返回 401让前端跳回登录页。注意JWT 的 payload 是 Base64 编码不是加密所以不要往里面塞密码等敏感信息。3.2 菜品与分类的增删改查菜品管理是这个项目里最典型的 CRUD 模块。表结构一般包含菜品表、分类表、口味表菜品和分类是多对一菜品和口味是一对多。写接口的时候新增菜品要同时插入菜品表和口味表所以要用事务保证一致性。Transactional public void saveWithFlavor(DishDTO dishDTO) { // 先插入菜品基本信息 Dish dish new Dish(); BeanUtils.copyProperties(dishDTO, dish); dishMapper.insert(dish); // 拿到自动生成的主键 Long dishId dish.getId(); // 再插入口味列表 ListDishFlavor flavors dishDTO.getFlavors(); if (flavors ! null !flavors.isEmpty()) { for (DishFlavor flavor : flavors) { flavor.setDishId(dishId); } dishFlavorMapper.insertBatch(flavors); } }Transactional注解保证两个插入操作要么都成功要么都回滚。BeanUtils.copyProperties是把 DTO 里的同名字段拷到实体类省去手动 set。insertBatch是批量插入比循环单条插入快很多口味多的时候差距明显。分页查询一般用 PageHelper 插件在查询前调用PageHelper.startPage(page, pageSize)后面紧跟的查询会自动分页。注意startPage之后必须紧跟查询语句中间不能插别的数据库操作否则分页会乱。3.3 订单状态流转与超时处理订单模块是整个项目里逻辑最绕的部分。一个订单从待付款、待接单、已接单、派送中到已完成中间还可能取消或退款。状态流转必须严格校验比如待付款的订单不能直接改成已完成。常见做法是用枚举定义状态在 service 层做前置判断public void confirmOrder(Long orderId) { Order order orderMapper.getById(orderId); // 只有待接单状态才能接单 if (!OrderStatus.TO_BE_CONFIRMED.equals(order.getStatus())) { throw new OrderBusinessException(订单状态不正确); } order.setStatus(OrderStatus.CONFIRMED); orderMapper.update(order); }超时未付款的订单需要自动取消一般用定时任务扫描。Spring Boot 里可以用Scheduled注解Scheduled(cron 0 * * * * ?) public void cancelTimeoutOrder() { // 查出下单超过 15 分钟且未付款的订单 ListOrder orders orderMapper.getTimeoutOrders(); for (Order order : orders) { order.setStatus(OrderStatus.CANCELLED); orderMapper.update(order); } }cron表达式0 * * * * ?表示每分钟执行一次。时间阈值 15 分钟要跟产品需求对齐改的时候注意别把已付款的订单也扫进去。4. 避坑与排查那些让我熬夜的报错4.1 数据库时区与中文乱码现象项目启动时报The server time zone value xxx is unrecognized或者存进去的中文变成???。原因MySQL 8.0 默认时区跟 JVM 不一致连接串没指定时区字符集没设成utf8mb4导致中文和 emoji 存不进去。解决连接串加上serverTimezoneAsia/Shanghai和characterEncodingutf-8数据库和表的字符集统一改成utf8mb4。4.2 Redis 连接超时现象启动时卡住最后报Unable to connect to Redis。原因Redis 服务没启动或者配置文件里的 host、port、password 跟实际不一致。Windows 版 Redis 有时候启动后闪退其实是配置文件路径不对。解决先确认redis-cli能连上再检查application.yml里的 Redis 配置。如果 Redis 设了密码password字段不能漏。4.3 前端请求跨域被拦截现象浏览器控制台报Access-Control-Allow-Origin相关错误接口返回 403。原因后端没配跨域或者前端代理没生效。有时候后端配了CrossOrigin但拦截器里又拦了 OPTIONS 请求导致预检失败。解决后端加全局跨域配置拦截器里放行 OPTIONS 请求前端确认vue.config.js的代理配置正确改完要重启前端服务。4.4 文件上传路径找不到现象上传图片时报FileNotFoundException或图片存到了临时目录重启后丢失。原因上传路径写的是相对路径实际运行目录跟预期不一致或者路径分隔符在 Windows 和 Linux 下不兼容。解决用绝对路径配置上传目录或者在配置文件里定义upload.path代码里用System.getProperty(user.dir)拼路径。部署到 Linux 时注意目录权限。4.5 定时任务重复执行现象超时订单被取消了两次或者日志里同一个任务打印了多遍。原因项目部署了多个实例每个实例的定时任务都跑了一遍或者Scheduled的方法被代理后重复调用。解决单机部署时问题不大多实例部署要引入分布式锁用 Redis 的setnx保证同一时间只有一个实例执行任务。5. 进阶技巧用接口文档和日志把调试效率提上去项目跑通之后真正拉开差距的是调试效率。我一般会做两件事一是把 Knife4j 或 Swagger 配好所有接口在线就能测不用来回切 Postman二是把日志级别调对关键业务打 info异常打 error排查问题时直接看日志定位。Knife4j 的依赖加进去之后访问http://localhost:8080/doc.html就能看到所有接口。给 Controller 加注解Api(tags 菜品管理) RestController RequestMapping(/admin/dish) public class DishController { ApiOperation(新增菜品) PostMapping public Result save(RequestBody DishDTO dishDTO) { dishService.saveWithFlavor(dishDTO); return Result.success(); } }Api和ApiOperation注解会让文档里显示中文说明前端联调时不用再问你这个接口干什么的。参数如果是对象Swagger 会自动展开字段你可以在 DTO 字段上加ApiModelProperty写清楚含义和是否必填。日志方面在application.yml里指定包路径的级别logging: level: com.example.mapper: debug com.example.service: infodebug级别会把 MyBatis 执行的 SQL 和参数都打出来写复杂查询时特别有用。但生产环境别开 debug日志量太大会拖慢性能。还有一个我踩过的坑接口返回的时间字段格式不对前端显示成一串数字。解决办法是在配置文件里加spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样所有Date类型字段序列化时都会按这个格式输出不用每个字段单独加注解。从那以后我每次拿到一个新项目都强制自己先跑通、再看日志、最后才改代码顺序反了就会在环境问题上浪费大量时间。希望这份笔记能帮你把苍穹外卖顺利跑起来少熬几个夜。本文还有配套的精品资源点击获取