
前言
为期十二天的“苍穹外卖”项目学习已圆满结束!!!在此期间,我系统性地完成了从前端界面搭建、接口联调、身份认证、缓存优化到定时任务、实时通信等核心功能的开发与调试。每一天的学习都围绕一个明确的技术主题展开,从Nginx部署到ECharts可视化,从微信登录到JWT认证,再到AOP、Redis、WebSocket等高级特性,逐步构建起一个高可用、可扩展的分布式外卖系统原型。
“苍穹外卖”作为一个典型的O2O(Online to Offline)餐饮服务平台,其业务价值与技术代表性不言而喻。它完整实现了从用户点餐、商家接单、订单调度到数据统计的全链路业务闭环,涵盖了管理端Web应用、用户端微信小程序以及后端Spring Boot服务.
本篇总结文档的编写,旨在对这十二天的高强度、系统性学习进行一次全面的复盘。其核心目标有三:
- 一是理清整个项目的整体架构思路,将零散的技术点串联成一个有机的整体;
- 二是系统化地沉淀技术要点,形成一份服务于高效复习、便于知识回溯的参考资料;
- 三是通过深度反思,提炼出项目中蕴含的思想,为后续的技术成长与架构演进奠定坚实基础。
文档将严格遵循“由表及里、从界面到逻辑再到数据”的认知路径,划分为“前端客户端(管理端)”、“前端用户端(小程序)”和“后端服务”三大核心板块,确保内容结构清晰、逻辑严谨、阅读流畅。
第一部分:前端客户端(管理端)
前端客户端即系统管理后台,是餐厅管理员进行日常运营操作的核心平台。该模块基于Vue.js框架与Element UI组件库构建,采用前后端分离架构,通过Axios与后端RESTful API进行数据交互。所有静态资源由Nginx托管,并通过反向代理将API请求转发至后端服务。此外,管理端还集成了ECharts实现关键业务数据的可视化展示,为经营决策提供数据支持。
1. Nginx 反向代理与负载均衡
知识点描述
Nginx 是一个高性能的HTTP和反向代理服务器,同时支持IMAP/POP3/SMTP代理。在本项目中,其核心作用是作为反向代理(Reverse Proxy),接收来自客户端的请求,并根据配置规则将请求转发至后端真实服务器(如运行在8080端口的Spring Boot应用),再将后端响应返回给客户端。客户端仅与Nginx交互,对后端服务器的存在无感知。
反向代理的核心配置通过 location 指令实现路径匹配,并使用 proxy_pass 指令指定转发目标。例如: 
上述配置表示:当客户端访问 http://localhost/api/employee/login 时,Nginx 会将路径中的 /api/ 替换为 http://localhost:8080/admin/,形成真实请求地址 http://localhost:8080/admin/employee/login 并转发。
负载均衡是反向代理的高级功能,用于将请求分发到多个后端服务器实例,提升系统并发处理能力与可用性。通过 upstream 指令定义服务器组,并在 proxy_pass 中引用该组:

weight 参数定义了服务器的权重,权重越高,接收到的请求越多。Nginx 支持多种负载均衡策略,包括轮询(Round Robin)、加权轮询、ip_hash(基于IP哈希)和least_conn(最少连接)等。
Nginx 反向代理的主要优势包括:
- 安全隔离:隐藏后端服务器的真实IP和端口,防止直接攻击。
- 性能优化:可缓存静态资源或后端响应,减少后端压力,提升访问速度。
- 高可用性:通过负载均衡实现故障转移,单台服务器宕机不影响整体服务。
个人笔记与理解
我认识到,Nginx 的反向代理本质上是“门面模式”(Facade Pattern)在Web架构中的实践。它为复杂的后端集群提供了一个统一、简洁的访问入口,极大地简化了客户端的调用逻辑。
在项目初期,我曾尝试让前端直接访问 http://localhost:8080,但很快意识到其安全隐患。一旦后端端口暴露,任何了解接口规范的用户都可绕过前端直接调用,进行未授权操作。Nginx 就像一个“安全门卫”,所有请求必须经过它的检查与转发,从而实现了访问控制的第一道屏障。
此外,Nginx 的负载均衡能力为系统的水平扩展提供了可能。当业务量增长时,我们只需增加新的后端服务器实例,并将其加入 upstream 组,Nginx 会自动分发请求,而前端和后端代码无需任何修改。这种“无侵入式”的扩展能力,是现代高可用系统设计的关键。
我还深入分析了不同负载均衡策略的适用场景:
- 轮询:适用于服务器性能相近的场景,实现最简单的请求均摊。
- 加权轮询:适用于服务器性能差异较大的场景,高性能服务器可承担更多负载。
- ip_hash:适用于需要保持会话一致性的场景,确保同一用户的请求始终由同一台服务器处理。
- least_conn:适用于长连接或处理时间不均的场景,优先将请求分发给负载最低的服务器。
在实际部署中,我还探索了Nginx的其他高级功能。例如,通过 gzip on; 开启Gzip压缩,可以显著减少传输的HTML、CSS、JS文件体积,提升页面加载速度。通过 expires 指令设置静态资源的缓存过期时间,可以让浏览器缓存图片、字体等资源,减少重复请求。
我还配置了HTTPS,将HTTP请求重定向到HTTPS,以保障数据传输的安全性:
server {
listen 80;
server_name your-domain.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/private.key;
# 其他配置…
}
这些配置共同构成了一个生产级的Web服务器部署方案。
技术选型思考
选择Nginx作为反向代理和负载均衡器,主要基于其高性能、高稳定性和丰富的功能集。在本项目中,它解决了前后端分离部署的跨域问题,通过路径重写将 /api/ 请求转发至后端服务,同时隐藏了后端端口,提升了安全性。其负载均衡能力为未来的系统扩展预留了空间。缺点是配置语法相对复杂,需要一定的学习成本,且对于动态服务发现支持较弱,但在单体应用阶段已足够。
2. Apache ECharts 数据可视化
知识点描述
Apache ECharts 是一个基于JavaScript的开源可视化库,支持在PC和移动端流畅运行。其工作原理是:前端通过HTTP请求(如Axios)向后端发起数据查询,后端返回JSON格式的数据,前端将数据注入ECharts的 option 配置项中,ECharts库根据配置渲染出对应的图表。
option 是一个JSON对象,定义了图表的标题、坐标轴、图例、提示框和数据系列等。核心配置项包括:
- title:图表标题。
- tooltip:鼠标悬停时的提示框。
- legend:图例,用于区分不同数据系列。
- xAxis 和 yAxis:X轴和Y轴的配置。
- series:数据系列,包含实际数据和图表类型(如line、bar、pie)。
ECharts支持多种图表类型,包括折线图、柱状图、饼图、散点图、雷达图、地图等,能够满足绝大多数数据可视化需求。
个人笔记与理解
我认识到,数据可视化的核心价值在于“降低认知成本”。相比于阅读一串数字,人类大脑更容易从图形中快速识别趋势、对比和异常。在本项目中,我根据不同的分析目标选择了合适的图表类型:
- 折线图(Line Chart):用于展示营业额、订单量等指标随时间变化的趋势,清晰反映业务的周期性波动。
- 柱状图(Bar Chart):用于对比不同菜品的销量或不同时间段的业绩,直观展示差异。
- 饼图(Pie Chart):用于展示用户来源(如新老用户)或订单状态的占比,体现构成关系。
我深刻体会到前后端接口设计对可视化实现的决定性影响。后端必须提供结构清晰的API,例如返回一个日期数组和一个对应的数值数组,前端才能正确绑定到 xAxis.data 和 series.data。接口设计的混乱会直接导致前端图表渲染失败。
技术选型思考
选择ECharts作为可视化库,是因为其功能强大、文档完善、社区活跃,且支持丰富的交互和动画效果。在本项目中,它完美地满足了营业额统计、订单量趋势、菜品销量排行等业务场景的可视化需求。其模块化设计允许按需引入,减少打包体积。缺点是对于超大数据集,仍需配合后端分页或聚合来优化性能,不能完全依赖前端处理。
第二部分:前端用户端(小程序)
前端用户端即面向消费者的微信小程序,是用户完成浏览、点餐、下单和支付的核心入口。该模块基于微信原生框架开发,利用微信提供的强大生态能力,如一键登录、支付接口和消息推送。其核心流程包括:用户授权登录、浏览菜品、管理购物车、提交订单和查看订单状态。所有业务逻辑通过调用后端RESTful API实现,数据以JSON格式进行交换。
1. 微信小程序与登录认证
知识点描述
采用微信开发者工具进行用户端前端开发
微信小程序登录认证采用“授权码模式”(Authorization Code Flow),其核心流程如下:

个人笔记与理解
我认识到,该流程的安全性设计非常精妙。code 的一次性使用和短暂有效期,以及 appsecret 必须在服务端保管,共同构成了安全防线。即使 code 被截获,攻击者也无法在没有 appsecret 的情况下换取 openid。
这体现了“前后端职责分离”的设计思想:前端只负责用户交互和Token存储,不关心身份验证的细节;后端才是业务逻辑的核心,负责与微信平台通信、管理用户状态和生成安全令牌。这种分离使得系统结构清晰,前端代码简洁,后端逻辑可控。
我实现了Token的自动续期机制。JWT令牌通常有有效期。当用户在Token过期后发起请求,后端会返回401状态码。前端需要在 app.js 的全局请求拦截器中监听到401错误,然后自动重新调用登录流程,获取新的Token,并重新发起原请求,整个过程对用户透明。
// 在请求拦截器中
if (res.statusCode === 401) {
// 重新登录
await login();
// 重新发起原请求
return request(originalRequest);
}
这种机制极大地提升了用户体验,避免了用户频繁手动登录。
技术选型思考
选择微信登录认证,是因为它极大地降低了用户的注册和登录门槛,利用微信的高普及率和强身份验证,提升了转化率。在本项目中,它解决了用户身份识别这一核心问题,同时保证了安全性。结合JWT实现了无状态认证,便于后端水平扩展。缺点是用户必须依赖微信生态,对于非微信用户不友好,且 appsecret 的管理需要格外谨慎。
第三部分:后端服务
后端服务是整个“苍穹外卖”项目的核心引擎,基于Spring Boot框架构建,采用Maven多模块管理。它负责处理所有业务逻辑、数据持久化、接口安全和系统调度。后端技术栈涵盖了从基础的分层架构、依赖注入,到高级的JWT认证、AOP编程、Redis缓存、定时任务和WebSocket实时通信,完整体现了现代Java企业级应用的开发范式。
一、准备部分
1. Swagger、knife4j与Apifox
知识点描述
在现代前后端分离开发中,API文档的管理至关重要。本项目中,我们考察并实践了三种主流的API文档解决方案:Swagger、knife4j 和 Apifox。

- Apifox:是一个API设计、开发、测试一体化协作平台。它整合了Postman(接口调试)、Swagger(文档生成)、Mock(模拟数据)和JMeter(性能测试)等多种工具的功能,支持API文档、API调试、API Mock、API自动化测试等一体化工作流,特别适合追求高效协作的团队。

个人笔记与理解
集成knife4j,体验得到了显著提升。其现代化的UI设计、支持接口按模块分组、可折叠的侧边栏以及强大的全局参数(如Authorization头)自动注入功能,极大地提升了开发和测试效率。一个关键实践是使用@Api和@ApiOperation注解对Controller和方法进行清晰标注,确保生成的文档语义明确。
然而,knife4j仍是一个“文档工具”。在团队协作中,我们还需要进行接口调试、数据Mock和自动化测试。这时,Apifox展现出了其一体化平台的巨大优势。通过将Swagger的OpenAPI规范导入Apifox,我们可以在一个平台内完成:
- 设计:与前端、后端、测试共同评审API。
- 开发:后端开发时直接在Apifox中调试接口。
- 测试:前端可使用Apifox的Mock服务模拟后端数据,实现并行开发。
- 维护:API变更后,所有相关方都能及时收到通知。
技术选型思考
| 1 | Swagger | 个人项目、小型团队、需要标准OpenAPI输出 | 开源、标准、社区支持广泛、与Spring Boot集成简单 | 界面传统、功能单一、协作能力弱 |
| 2 | knife4j | 企业级项目、微服务架构、需要增强文档功能 | 界面美观、功能强大(导出、排序、参数管理)、无缝兼容Swagger | 仍是文档工具,缺乏调试、Mock等高级功能 |
| 3 | Apifox | 中大型团队、追求高效协作、需要全流程管理 | 一体化平台、支持Mock和自动化测试、团队协作体验好 | 商业软件 |
2.项目架构思想
知识点描述
“苍穹外卖”项目遵循核心架构思想,用到了maven的分模块开发。旨在构建一个高内聚、低耦合、可维护、可扩展的系统。其核心思想包括:

个人笔记与理解
在项目开发中,我深刻体会到“适合、简单、演化”这三大架构原则的重要性。在项目初期,我们没有盲目追求微服务或复杂的分布式架构,而是选择了简单可靠的单体架构。这极大地降低了开发和部署的复杂性,让我们能快速迭代,验证核心业务逻辑。
一个关键实践是严格遵守高内聚、低耦合原则。例如,在OrderService中,我只处理与订单相关的业务,如创建订单、查询订单、取消订单。而与支付相关的逻辑,则通过调用独立的支付服务(或模拟)来完成,而不是将支付代码混杂在订单逻辑中。这种设计使得代码更易于理解和测试。
另一个深刻反思是关于过度设计的风险。在实现一个简单的状态查询接口时,我曾考虑引入消息队列进行异步处理。但经过权衡,发现这会引入不必要的复杂性,因为查询操作本身是轻量级的。最终,我选择了直接查询数据库的简单方案。这让我认识到,能用简单方案解决的问题,绝不选复杂的方案[5]。
技术选型思考
选择当前的架构思想,是因为它完美匹配了项目的当前阶段和团队能力。对于一个学习和原型项目,简单、清晰的架构远比复杂、先进的架构更实用。分层和模块化设计为代码的可维护性提供了保障,前后端分离和RESTful API为团队协作提供了规范。
相比大厂的复杂微服务架构,我们的架构更注重实用性和可实现性。它避免了分布式事务、服务发现、配置中心等高级概念的困扰,让学习者能专注于核心业务逻辑和关键技术点的掌握。未来,当业务量和团队规模增长时,我们可以基于当前的单体架构,逐步进行拆分和演化。
3. 日志记录
知识点描述

日志记录是监控程序运行状态、排查错误和分析系统行为的重要手段。本项目使用Spring Boot默认集成的Logback作为日志框架。日志系统由三个核心组件构成:
- Logger:负责接收日志消息。
- Appender:决定日志输出位置(如控制台、文件)。
- Layout(或Formatter):定义日志输出格式。
Logback通过logback-spring.xml配置文件进行管理,支持日志级别(TRACE, DEBUG, INFO, WARN, ERROR)、日志文件滚动(按大小或时间)、日志格式自定义等。
个人理解与实践反思
在项目中,我最初只在关键位置使用System.out.println()进行调试,但很快发现这种方式难以管理,且无法持久化。引入Logback后,我能够系统地记录程序运行信息。
一个关键实践是合理使用日志级别:
- INFO:记录程序启动、关键业务流程(如“用户下单成功”)。
- DEBUG:记录详细流程,用于开发调试(如“查询数据库SQL: SELECT * FROM …”)。
- WARN:记录潜在问题,如“缓存未命中”。
- ERROR:记录异常和错误。
我配置了日志文件按天滚动,并保留最近30天的日志,防止磁盘被占满。同时,我为日志添加了清晰的格式,包含时间戳、线程名、日志级别、类名和日志内容,便于排查问题。
⚠️ 易错点:在循环中记录大量DEBUG日志可能导致性能急剧下降。解决方案是在生产环境中将日志级别调整为INFO或更高。
我还构想了未来的日志分析体系:将日志文件通过Filebeat采集,发送到Elasticsearch存储,并用Kibana进行可视化分析,实现系统的可观测性。
技术选型分析
选择Logback作为日志框架,是因为它是SLF4J的原生实现,性能优秀,且与Spring Boot无缝集成。相比其他日志框架(如Log4j2),Logback配置更灵活,社区支持良好。其模块化设计和强大的过滤功能,足以满足本项目的需求。
二、安全部分
1. JWT 令牌与身份认证
知识点描述
JWT(JSON Web Token)是一种开放标准(RFC 7519),由三部分组成,用点(.)分隔:
工作流程:
个人笔记与理解
我认识到,JWT的“无状态”是其最大优势。传统Session需要在服务器端存储,难以水平扩展。而JWT将状态“编码”在令牌本身,服务器只需一个密钥即可验证,非常适合分布式系统。
然而,无状态也带来挑战:无法主动使令牌失效。对于用户主动退出等场景,需配合Redis实现“黑名单”机制,但这又部分牺牲了无状态性。因此,JWT的过期时间(TTL)设置需权衡安全与用户体验。
在实现中,使用了 jjwt 库来生成和解析JWT。一个典型的生成代码如下:
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims
);
其中 claims 是一个Map,包含了要放入Payload的自定义信息。
我还深入研究了JWT的安全性。除了使用强密钥(secret)外,还应确保密钥的保密性。在生产环境中,密钥不应硬编码在代码中,而应通过环境变量或配置中心(如Nacos、Apollo)动态获取。
技术选型思考
选择JWT作为身份认证机制,是因为它无状态、可扩展,非常适合前后端分离和分布式架构。在本项目中,它解决了管理端和用户端的统一认证问题,通过不同的密钥和TTL区分管理员和普通用户。优点是服务端无需存储会话,减轻了服务器压力。缺点是令牌一旦签发,在过期前无法主动失效,对于安全性要求极高的场景,需要额外的机制(如Redis黑名单)来弥补。
2. 过滤器(Filter)与拦截器(Interceptor)
知识点描述
| 1 | 来源 | Java Servlet规范 | Spring MVC框架 |
| 2 | 作用范围 | 所有进入容器的请求(Servlet、JSP、静态资源) | 仅Spring MVC的Controller方法 |
| 3 | 执行时机 | 在请求到达DispatcherServlet之前 | 在HandlerAdapter调用Handler之前 |
| 4 | 配置方式 | @WebFilter + @ServletComponentScan 或 web.xml | 实现 HandlerInterceptor 并在配置类中注册 |
个人笔记与理解
我认识到,两者的主要区别在于作用范围和所属框架。Filter是更底层的Java Web组件,而Interceptor是Spring MVC特有的。
在本项目中,我选择Interceptor实现登录校验,因为校验只针对需要用户身份的业务接口(Controller方法),对于静态资源(如CSS、JS)无需校验。使用Interceptor可以精确控制,避免不必要的性能开销。
在实现过程中,我遇到了一个性能问题:在高并发下,ThreadLocal 的使用可能导致内存泄漏。因为Tomcat使用线程池,线程会被复用。如果在 preHandle 中存入 ThreadLocal 后,没有在 afterCompletion 中清理,那么下一个请求可能会获取到上一个请求的用户ID,造成严重的安全漏洞。
为了解决这个问题,我严格遵循了“存入即清理”的原则,在 afterCompletion 方法中调用 BaseContext.removeCurrentId()。
技术选型思考
选择Interceptor而非Filter来实现登录校验,是因为其作用范围更精确,仅针对Controller层,避免了对静态资源的无效校验,提升了性能。同时,它能更好地与Spring生态集成,可以方便地使用依赖注入。Filter更适合处理编码、日志等更底层的通用逻辑。
三、异常处理部分
1. 全局异常处理
知识点描述
在Spring Boot中,通过 @ControllerAdvice 和 @ExceptionHandler 注解实现全局异常处理。@ControllerAdvice 将一个类标记为全局异常处理器,@ExceptionHandler 指定处理的异常类型。
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public Result<String> handleBusinessException(BusinessException ex) {
return Result.error(ex.getMessage());
}
}
个人笔记与理解
我认识到,全局异常处理是提升系统健壮性和用户体验的关键。它将散落在各处的异常处理逻辑集中管理,避免了在每个Controller中重复编写try-catch代码。
在本项目中,定义了 BusinessException 来表示业务逻辑异常(如“账号不存在”、“密码错误”),并通过全局处理器将其转换为统一的响应格式。对于系统异常(如 NullPointerException),也提供了兜底处理,返回友好的错误提示,而不是暴露堆栈信息。
还实现了异常日志的记录,将异常信息输出到日志文件,便于后续排查问题。
技术选型思考
选择Spring Boot的全局异常处理机制,是因为它简单、强大且与框架深度集成。在本项目中,它统一了所有接口的错误响应格式,保证了API的规范性。优点是代码简洁,维护成本低。缺点是如果异常类型定义不清晰,可能导致错误信息不够具体。
四、数据交互部分
1. 统一结果响应
知识点描述
为了保证前后端交互的规范性,后端定义了统一的响应结果类 Result<T>,包含状态码、提示信息和数据。
public class Result<T> {
private Integer code; // 1 成功, 0 失败
private String msg;
private T data;
// 构造方法、getters、setters
public static <T> Result<T> success(T object) {
Result<T> result = new Result<>();
result.code = 1;
result.data = object;
return result;
}
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.code = 0;
result.msg = msg;
return result;
}
}
个人笔记与理解
我认识到,统一响应格式是前后端协作的“契约”。它让前端可以基于固定的结构编写通用的处理逻辑,无需关心每个接口的具体实现。
在项目中,所有Controller方法的返回值都封装为 Result<T>。结合全局异常处理,无论是业务成功还是失败,前端都能收到结构一致的响应,极大简化了前端的错误处理逻辑。
我还实现了 @ControllerAdvice 与 ResponseBodyAdvice 的结合,可以自动将返回值包装成 Result,但考虑到灵活性,本项目选择在每个方法中显式返回。
技术选型思考
选择定义 Result<T> 作为统一响应格式,是因为它清晰、规范,易于前端解析。在本项目中,它解决了接口响应格式不统一的问题,提升了开发效率。优点是结构简单,易于理解。缺点是增加了额外的包装层级,对于简单的场景可能显得冗余。
2. 参数校验
知识点描述
使用Hibernate Validator的注解(如 @NotNull, @Length, @Pattern)对Controller方法的参数进行校验。
@PostMapping("/login")
public Result<EmployeeLoginVO> login(@RequestBody @Valid EmployeeLoginDTO employeeLoginDTO) {
// …
}
在DTO类上使用注解:
public class EmployeeLoginDTO {
@NotBlank private String username;
@NotBlank private String password;
}
个人笔记与理解
我认识到,参数校验是系统安全的第一道防线。它防止了非法或恶意数据进入业务逻辑层,避免了潜在的错误和安全漏洞。
在项目中,我将校验逻辑放在DTO层,通过 @Valid 注解触发。校验失败时,会抛出 MethodArgumentNotValidException,由全局异常处理器捕获并返回错误信息。
我还使用了自定义校验注解,例如 @Phone,来校验手机号码格式,提高了代码的复用性。
技术选型思考
选择Hibernate Validator进行参数校验,是因为它标准、成熟,且与Spring Boot无缝集成。在本项目中,它保证了接口输入数据的合法性。优点是声明式编程,代码简洁。缺点是对于复杂的业务校验逻辑,仍需在Service层手动实现。
3.HTTP协议
知识点描述
HTTP(HyperText Transfer Protocol)是应用层协议,用于客户端(浏览器、APP)与服务器之间的数据交互。它是一种简单的请求-响应协议,通常运行在TCP之上。
HTTP请求由三部分组成:
HTTP响应也由三部分组成:
HTTP协议具有无状态性,即每次请求都是独立的。为了保持用户状态,需要借助Cookie、Session或Token机制。
个人理解与实践反思
深入理解HTTP协议后,我对RESTful API的设计有了更深刻的认识。例如:
- 使用GET /api/orders获取订单列表。
- 使用POST /api/orders创建新订单。
- 使用PUT /api/orders/{id}更新订单。
- 使用DELETE /api/orders/{id}删除订单。
这种设计语义清晰,符合HTTP方法的本意。
在调试接口时,我经常使用浏览器的开发者工具或Postman查看HTTP请求和响应的详细信息。通过分析Content-Type,我确保了前后端数据格式的一致性(如application/json)。通过检查状态码,我能快速判断请求是成功(2xx)、客户端错误(4xx)还是服务器错误(5xx)。
理解无状态性后,我更好地设计了JWT认证方案:客户端在登录后获取Token,并在后续请求的Authorization头中携带它,服务器通过解析Token来识别用户身份。
技术选型分析
选择HTTP协议作为基础通信协议,因其标准化程度高、生态系统完善、浏览器原生支持。相比其他应用层协议如FTP、SMTP,HTTP更适合Web应用的数据传输需求。虽然HTTP/1.1存在队头阻塞等问题,但通过升级到HTTP/2或HTTP/3可有效解决。HTTPS作为HTTP的安全版本,通过SSL/TLS加密保障数据传输安全,已成为现代Web应用的标准选择。
4.Apache POI
知识点描述

Apache POI是Java平台上的一个开源库,用于读写Microsoft Office格式的文件,包括Excel(.xls和.xlsx)、Word(.doc和.docx)和PowerPoint(.ppt和.pptx)。对于Excel操作,Apache POI提供了:
- HSSFWorkbook:用于03版本.xls文件。
- XSSFWorkbook:用于07版本.xlsx文件。
- SXSSFWorkbook:基于流式处理的XSSFWorkbook,用于处理大数据量的Excel文件导出,可以有效控制内存使用。
个人理解与实践反思
在项目中,我实现了订单报表的导出功能。管理员可以在管理端选择日期范围,点击“导出Excel”按钮,系统会生成一个包含订单号、金额、状态、下单时间等信息的Excel文件。
核心代码流程如下:
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {
// 1. 查询数据
List<OrderVO> orders = orderService.getAllOrders();
// 2. 创建工作簿
SXSSFWorkbook workbook = new SXSSFWorkbook();
Sheet sheet = workbook.createSheet("订单报表");
// 3. 写入表头
Row headerRow = sheet.createRow(0);
headerRow.createCell(0).setCellValue("订单号");
headerRow.createCell(1).setCellValue("金额");
// … 其他表头
// 4. 写入数据
int rowNum = 1;
for (OrderVO order : orders) {
Row row = sheet.createRow(rowNum++);
row.createCell(0).setCellValue(order.getOrderNumber());
row.createCell(1).setCellValue(order.getAmount());
// … 其他数据
}
// 5. 写出文件
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=orders.xlsx");
workbook.write(response.getOutputStream());
workbook.close(); // 注意关闭
}
技术选型分析
选择Apache POI,是因为它是Java领域处理Office文档的事实标准,功能强大且免费。对于本项目中的Excel导出需求,它完全胜任。SXSSFWorkbook的流式处理特性,使其能够处理远超JVM内存限制的数据量,是处理大数据导出的理想选择。
五、性能优化部分
1. Redis 缓存与 Spring Cache
知识点描述
Redis是一种键值型的NoSql数据库。

Redis是一个高性能的内存键值存储系统。本项目使用Spring Cache抽象层,通过注解简化缓存操作:
- @EnableCaching:开启缓存。
- @Cacheable:查询时先查缓存,缓存有则返回,无则执行方法并缓存结果。
- @CacheEvict:删除缓存,通常在更新/删除操作后使用。
@Cacheable(cacheNames = "dishCache", key = "#categoryId")
public List<DishVO> getDishList(Long categoryId) {
// 查询数据库的逻辑
}
个人笔记与理解
我认识到,缓存的本质是“用空间换时间”。但随之而来的是“缓存与数据库一致性”问题。本项目采用“Cache Aside”模式:读请求先查缓存,缓存失效再查数据库;写请求先更新数据库,再删除缓存。
我还深入分析了缓存三大问题:
- 穿透:查询不存在的数据。对策:缓存空值。
- 击穿:热点Key过期瞬间大量请求涌入。对策:加互斥锁。
- 雪崩:大量Key同时过期。对策:设置随机过期时间。
在实现菜品缓存时,我使用了 @Cacheable 注解。当用户第一次点击“川菜”分类时,key 为 categoryId 的缓存不存在,方法会执行数据库查询,并将结果(一个DishVO列表)序列化后存入Redis。当第二个用户也点击“川菜”时,缓存命中,直接从Redis返回数据,无需查询数据库。
我还实现了缓存预热。在系统启动时,主动将一些热点数据(如首页推荐菜品)加载到缓存中,避免上线后第一个请求触发慢查询。
@Component
public class CachePreloader implements ApplicationRunner {
@Autowired
private DishService dishService;
@Override
public void run(ApplicationArguments args) throws Exception {
// 预热热门分类的菜品
dishService.getDishList(1L); // 假设1L是热门分类
dishService.getDishList(2L);
}
}
技术选型思考
选择Redis作为缓存中间件,是因为其性能极高(微秒级响应)、数据结构丰富、支持持久化。结合Spring Cache,极大地简化了开发。在本项目中,它显著提升了菜品列表、分类等高频查询接口的响应速度。优点是集成简单,性能提升明显。缺点是引入了新的组件,增加了系统复杂性,且需要处理缓存一致性问题。
2. AOP 与公共字段自动填充
知识点描述

AOP(面向切面编程)通过“切面”(Aspect)将分散在各处的共性功能(如日志、事务)集中管理。本项目中,通过自定义 @AutoFill 注解和AOP切面,在执行INSERT/UPDATE操作时,自动为 create_time, create_user 等字段赋值。
实现步骤:
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {}
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
// 获取数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
// 获取实体对象
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) return;
Object entity = args[0];
// 自动填充
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
if (operationType == OperationType.INSERT) {
ReflectionUtils.setField(entity, "createTime", now);
ReflectionUtils.setField(entity, "createUser", currentId);
ReflectionUtils.setField(entity, "updateTime", now);
ReflectionUtils.setField(entity, "updateUser", currentId);
} else if (operationType == OperationType.UPDATE) {
ReflectionUtils.setField(entity, "updateTime", now);
ReflectionUtils.setField(entity, "updateUser", currentId);
}
}
个人笔记与理解
我认识到,AOP的底层是动态代理。Spring在运行时为目标对象创建代理,当调用目标方法时,先执行切面逻辑,再执行真实方法。这实现了对核心业务逻辑的“无侵入式”增强。 
“约定优于配置”在此体现:我们约定所有需要填充的Mapper方法都必须标注 @AutoFill,切面则自动生效。这避免了在每个Service方法中手动赋值,极大提升了开发效率。
在实现过程中,我遇到了一个棘手问题:当一个Service方法内部调用另一个被 @AutoFill 注解的方法时,AOP失效。这是因为内部调用不经过代理对象,而是直接调用目标对象的方法。
为了解决这个问题,我将被 @AutoFill 注解的方法移到了另一个Service中,或者通过 ApplicationContext 获取代理对象来调用。
技术选型思考
选择AOP实现公共字段自动填充,是因为它能彻底消除重复代码,实现“一次编写,处处生效”。在本项目中,它解决了在每个Service中手动设置创建/更新时间的繁琐问题。优点是代码简洁,维护性高。缺点是引入了反射,有一定性能开销,且调试相对复杂。
六、异步与实时通信部分
1. Spring Task 定时任务
知识点描述
Spring Task通过 @Scheduled 注解和Cron表达式定义定时任务。Cron表达式格式为 秒 分 时 日 月 周,例如 0 * * * * ? 表示每分钟执行一次。 cron表达式在线生成器:https://cron.qqe2.com/
@Component
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder() {
log.info("处理超时订单: {}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(–15);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
if (ordersList != null && ordersList.size() > 0) {
for (Orders orders : ordersList) {
orders.setStatus(Orders.CANCELLED);
orders.setCancelReason("订单超时,自动取消");
orders.setCancelTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
}
个人笔记与理解
我认识到,定时任务解放了人力,让系统能“自主”运行。在本项目中,通过定时任务每分钟扫描超时订单并自动取消,保证了订单状态的及时更新。
编写Cron表达式时,我掌握了关键符号:*(任意)、?(不指定)、-(范围)、/(步长)。例如 0 0/5 * * * ? 表示每5分钟执行一次。
我还实现了定时任务的动态管理。通过将Cron表达式存储在数据库或配置中心,可以在不重启应用的情况下动态调整任务的执行频率。
@Scheduled(cron = "${task.order.timeout.cron}")
public void processTimeoutOrder() {
// …
}
在 application.yml 中:
task:
order:
timeout:
cron: 0 * * * * ?
这种设计极大地提升了系统的灵活性。
技术选型思考
选择Spring Task作为定时任务框架,是因为它轻量、简单,且与Spring Boot无缝集成。在本项目中,它完美地满足了处理超时订单、数据统计等周期性任务的需求。优点是开发效率高。缺点是默认单线程执行,对于耗时任务会阻塞,且在集群环境下会重复执行,需要额外的分布式锁机制来解决。
2. WebSocket 实时通信
知识点描述

WebSocket是一种全双工通信协议,允许服务器在连接建立后主动向客户端推送消息。在Spring中,使用 @ServerEndpoint 注解定义端点。
@ServerEndpoint("/ws/{sid}")
@Component
public class WebSocketServer {
private static Map<String, Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
sessionMap.put(sid, session);
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到来自客户端的消息:" + message);
}
@OnClose
public void onClose(@PathParam("sid") String sid) {
sessionMap.remove(sid);
}
public void sendToAllClient(String message) {
for (Session session : sessionMap.values()) {
try {
session.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
个人笔记与理解
我认识到,WebSocket解决了HTTP“请求-响应”模式的局限性。在没有WebSocket时,只能通过轮询(Polling)模拟实时,造成巨大资源浪费。而WebSocket建立长连接后,通信开销极小。
@ServerEndpoint 由Java EE规范定义,不被Spring容器直接管理。因此,需要 @Component 和 ServerEndpointExporter Bean来将其注册为Spring Bean。
在实现来单提醒功能时,当用户在小程序端下单,后端订单服务会调用WebSocket服务,将订单信息推送给管理端的浏览器。
@Autowired
private WebSocketServer webSocketServer;
@Transactional
public void submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
// … 保存订单
webSocketServer.sendToAllClient("您有新的订单,请及时处理!");
}
这种实时推送机制,极大地提升了商家的响应速度。
技术选型思考
选择WebSocket作为实时通信方案,是因为它真正实现了服务端主动推送,效率远高于轮询。在本项目中,它解决了来单提醒、客户催单等需要实时通知的业务场景。优点是实时性强,资源消耗低。缺点是连接管理复杂,需要处理断线重连、心跳检测等问题,且在分布式环境下需要共享会话状态。
总结
“苍穹外卖”项目的学习之旅,是一次从理论到实践、从零散知识点到系统化认知的深刻蜕变。通过十二天的高强度开发,我不仅掌握了Nginx、Vue、微信小程序、Spring Boot、Redis、WebSocket等主流技术的使用,更深入理解了它们在真实项目中的协同关系与工程价值。
三大技术模块的协同关系构成了一个完整的闭环:前端客户端(管理端)为商家提供数据可视化与运营管理界面;前端用户端(小程序)为消费者提供便捷的点餐入口;后端服务则作为核心引擎,处理所有业务逻辑、数据存储与系统调度。三者通过RESTful API和WebSocket进行通信,共同支撑起整个外卖平台的运行。
项目中体现的思想尤为珍贵:
- 解耦与分层:通过Maven多模块和Controller-Service-Mapper三层架构,实现了高内聚、低耦合,提升了代码的可维护性。
- 缓存:利用Redis缓存热点数据,显著提升了系统性能,保护了数据库。
- 异步与定时:通过Spring Task实现后台自动化任务,解放了人力,保证了业务的及时性。
- 安全与认证:通过JWT实现无状态认证,通过Nginx反向代理保障系统安全。
在技术成长之外,我的开发流程意识也得到强化。从需求分析、技术选型、接口设计到编码实现、联调测试,我体验了完整的软件开发生命周期。我学会了如何阅读官方文档、如何调试复杂问题、如何通过日志定位异常。
然而,本项目仍是一个单体应用的原型。后续优化方向包括:






