苍穹外卖-day01
课程内容
-
软件开发整体介绍
-
苍穹外卖项目介绍
-
开发环境搭建
-
导入接口文档
-
Swagger
🧠 理论理解
-
本节主要带领大家认识软件开发的整体流程,了解实际项目结构和开发环境。
-
通过苍穹外卖项目,系统掌握前后端分离开发模式、接口管理、Swagger接口文档生成,打好企业实战基础。
🏢 企业实战理解
-
字节跳动、阿里等公司新员工入职培训中,常会以完整项目(如电商外卖平台)作为实践项目做个小抖音,快速熟悉企业开发流程。
-
OpenAI、Google在内部也用小型系统(如Food Delivery API)训练新工程师掌握微服务架构与接口协作。
项目整体效果展示:
管理端-外卖商家使用
用户端-点餐用户使用
当我们完成该项目的学习,可以培养以下能力:
1. 软件开发整体介绍
作为一名软件开发工程师,我们需要了解在软件开发过程中的开发流程, 以及软件开发过程中涉及到的岗位角色,角色的分工、职责, 并了解软件开发中涉及到的三种软件环境。那么这一小节,我们将从 软件开发流程、角色分工、软件环境 三个方面整体介绍一下软件开发。
1.1 软件开发流程
1). 第1阶段: 需求分析
完成需求规格说明书、产品原型编写。
需求规格说明书, 一般来说就是使用 Word 文档来描述当前项目的各个组成部分,如:系统定义、应用环境、功能规格、性能需求等,都会在文档中描述。例如:
产品原型,一般是通过网页(html)的形式展示当前的页面展示什么样的数据, 页面的布局是什么样子的,点击某个菜单,打开什么页面,点击某个按钮,出现什么效果,都可以通过产品原型看到。 例如:
2). 第2阶段: 设计
设计的内容包含 UI设计、数据库设计、接口设计。
UI设计:用户界面的设计,主要设计项目的页面效果,小到一个按钮,大到一个页面布局,还有人机交互逻辑的体现。例如:
数据库设计:需要设计当前项目中涉及到哪些数据库,每一个数据库里面包含哪些表,这些表结构之间的关系是什么样的,表结构中包含哪些字段。例如:
接口设计:通过分析原型图,首先,粗粒度地分析每个页面有多少接口,然后,再细粒度地分析每个接口的传入参数,返回值参数,同时明确接口路径及请求方式。例如:
3). 第3阶段: 编码
编写项目代码、并完成单元测试。
项目代码编写:作为软件开发工程师,我们需要对项目的模块功能分析后,进行编码实现。
单元测试:编码实现完毕后,进行单元测试,单元测试通过后再进入到下一阶段。例如:
4). 第4阶段: 测试
在该阶段中主要由测试人员, 对部署在测试环境的项目进行功能测试, 并出具测试报告。
5). 第5阶段: 上线运维
在项目上线之前, 会由运维人员准备服务器上的软件环境安装、配置, 配置完毕后, 再将我们开发好的项目,部署在服务器上运行。
🧠 理论理解
-
软件开发标准流程包括:需求分析 ➔ 设计 ➔ 编码 ➔ 测试 ➔ 上线运维。
-
每一个阶段环环相扣,确保最终交付的软件符合用户需求且质量可靠。
🏢 企业实战理解
-
阿里巴巴、腾讯内部使用敏捷开发(Scrum)管理流程,每周迭代一轮,小步快跑。
-
OpenAI在GPT模型部署中采用迭代式需求验证,每次模型上线前有严格的测试和安全审查环节。
1.2 角色分工
在对整个软件开发流程熟悉后, 我们还有必要了解一下在整个软件开发流程中涉及到的岗位角色,以及各个角色的职责分工。
岗位/角色 | 对应阶段 | 职责/分工 |
---|---|---|
项目经理 | 全阶段 | 对整个项目负责,任务分配、把控进度 |
产品经理 | 需求分析 | 进行需求调研,输出需求调研文档、产品原型等 |
UI设计师 | 设计 | 根据产品原型输出界面效果图 |
架构师 | 设计 | 项目整体架构设计、技术选型等 |
开发工程师 | 编码 | 功能代码实现 |
测试工程师 | 测试 | 编写测试用例,输出测试报告 |
运维工程师 | 上线运维 | 软件环境搭建、项目上线 |
上述我们讲解的角色分工, 是在一个项目组中比较标准的角色分工, 但是在实际的项目中, 有一些项目组由于人员配置紧张, 可能并没有专门的架构师或测试人员, 这个时候可能需要有项目经理或者程序员兼任。
🧠 理论理解
-
软件开发团队通常包含:项目经理、产品经理、UI设计师、架构师、开发工程师、测试工程师、运维工程师。
-
小团队中角色可能合并,大项目中则细分更明确。
🏢 企业实战理解
-
字节跳动在推荐系统项目组中,开发与测试是分开的,严格走独立职责线,避免开发人员自测上线。
-
亚马逊AWS工程文化强调“两个披萨原则”(团队小而高效),一般5-8人即可独立负责一个微服务模块。
1.3 软件环境
作为软件开发工程师,在编码的过程中就不可避免地会接触多种软件环境,我们主要来分析在工作中经常遇到的三套环境, 分别是: 开发环境、测试环境、生产环境。 接下来,我们分别介绍一下这三套环境的作用和特点。
1). 开发环境(development)
我们作为软件开发人员,在开发阶段使用的环境,就是开发环境,一般外部用户无法访问。
比如,我们在开发中使用的MySQL数据库和其他的一些常用软件,我们可以安装在本地, 也可以安装在一台专门的服务器中, 这些应用软件仅仅在软件开发过程中使用, 项目测试、上线时,我们不会使用这套环境了,这个环境就是开发环境。
2). 测试环境(testing)
当软件开发工程师,将项目的功能模块开发完毕,并且单元测试通过后,就需要将项目部署到测试服务器上,让测试人员对项目进行测试。那这台测试服务器就是专门给测试人员使用的环境, 也就是测试环境,用于项目测试,一般外部用户无法访问。
3). 生产环境(production)
当项目开发完毕,并且由测试人员测试通过之后,就可以上线项目,将项目部署到线上环境,并正式对外提供服务,这个线上环境也称之为生产环境。
开发环境 测试环境 生产环境
首先,会在开发环境中进行项目开发,往往开发环境大多数都是本地的电脑环境和局域网内的环境,当开发完毕后,然后会把项目部署到测试环境,测试环境一般是一台独立测试服务器的环境,项目测试通过后,最终把项目部署到生产环境,生产环境可以是机房或者云服务器等线上环境。
🧠 理论理解
-
软件环境通常分为:
-
开发环境:程序员本地/内部开发服务器
-
测试环境:专门给测试工程师测试功能用
-
生产环境:正式对外提供服务的服务器环境
-
🏢 企业实战理解
-
腾讯云:开发环境基于内网测试集群,测试环境严格隔离生产环境。
-
Google生产环境的隔离级别极高,开发者无法直接访问生产数据库,必须通过专门的审计机制授权。
2. 苍穹外卖项目介绍
在开发苍穹外卖这个项目之前,我们需要全方位的来介绍一下当前我们学习的这个项目。接下来,我们将从项目简介、产品原型、技术选型三个方面来介绍苍穹外卖这个项目。
2.1 项目介绍
本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台 和 小程序端应用 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。
接下来,通过功能架构图来展示管理端和用户端的具体业务功能模块。
1). 管理端功能
员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 ,数据统计,来单提醒。
2). 用户端功能
微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 支付、分类及菜品浏览。
🧠 理论理解
-
苍穹外卖项目是一个典型的外卖点餐系统,覆盖了管理后台(商家端)和用户小程序端(消费者端)两套系统。
-
包含用户登录、菜品管理、订单管理、支付功能等核心模块。
🏢 企业实战理解
-
美团、饿了么早期开发中也是通过拆分【用户端】与【商家端】并独立迭代。
-
字节跳动的字节外卖(内部应用)最初原型也采用类似微前端架构和分端部署方式。
2.2 产品原型
产品原型,用于展示项目的业务功能,一般由产品经理进行设计。
注意事项: 产品原型主要用于展示项目的功能,并不是最终的页面效果。
在课程资料的产品原型文件夹下,提供了两份产品原型。
管理端原型图:
用户端原型图:
1). 管理端
餐饮企业内部员工使用。 主要功能有:
模块 | 描述 |
---|---|
登录/退出 | 内部员工必须登录后,才可以访问系统管理后台 |
员工管理 | 管理员可以在系统后台对员工信息进行管理,包含查询、新增、编辑、禁用等功能 |
分类管理 | 主要对当前餐厅经营的 菜品分类 或 套餐分类 进行管理维护, 包含查询、新增、修改、删除等功能 |
菜品管理 | 主要维护各个分类下的菜品信息,包含查询、新增、修改、删除、启售、停售等功能 |
套餐管理 | 主要维护当前餐厅中的套餐信息,包含查询、新增、修改、删除、启售、停售等功能 |
订单管理 | 主要维护用户在移动端下的订单信息,包含查询、取消、派送、完成,以及订单报表下载等功能 |
数据统计 | 主要完成对餐厅的各类数据统计,如营业额、用户数量、订单等 |
2). 用户端
移动端应用主要提供给消费者使用。主要功能有:
模块 | 描述 |
---|---|
登录/退出 | 用户需要通过微信授权后登录使用小程序进行点餐 |
点餐-菜单 | 在点餐界面需要展示出菜品分类/套餐分类, 并根据当前选择的分类加载其中的菜品信息, 供用户查询选择 |
点餐-购物车 | 用户选中的菜品就会加入用户的购物车, 主要包含 查询购物车、加入购物车、删除购物车、清空购物车等功能 |
订单支付 | 用户选完菜品/套餐后, 可以对购物车菜品进行结算支付, 这时就需要进行订单的支付 |
个人信息 | 在个人中心页面中会展示当前用户的基本信息, 用户可以管理收货地址, 也可以查询历史订单数据 |
🧠 理论理解
-
产品原型用于描述系统交互流程,是开发前的重要参考资料。
-
原型图主要展示界面布局、页面跳转逻辑,不追求最终UI美观。
🏢 企业实战理解
-
阿里巴巴UED团队使用Axure设计高保真原型,并通过蓝湖(Lanhu)工具协作管理设计稿。
-
OpenAI内部也会用Figma等工具快速构建新功能原型,再交给开发团队评估技术可行性。
2.3 技术选型
关于本项目的技术选型, 我们将会从 用户层、网关层、应用层、数据层 这几个方面进行介绍,主要用于展示项目中使用到的技术框架和中间件等。
1). 用户层
本项目中在构建系统管理后台的前端页面,我们会用到H5、Vue.js、ElementUI、apache echarts(展示图表)等技术。而在构建移动端应用时,我们会使用到微信小程序。
2). 网关层
Nginx是一个服务器,主要用来作为Http服务器,部署静态资源,访问性能高。在Nginx中还有两个比较重要的作用: 反向代理和负载均衡, 在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。
3). 应用层
SpringBoot: 快速构建Spring项目, 采用 "约定优于配置" 的思想, 简化Spring项目的配置开发。
SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。
Spring Task: 由Spring提供的定时任务框架。
httpclient: 主要实现了对http请求的发送。
Spring Cache: 由Spring提供的数据缓存框架
JWT: 用于对应用程序上的用户进行身份验证的标记。
阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。
Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。
POI: 封装了对Excel表格的常用操作。
WebSocket: 一种通信网络协议,使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。
4). 数据层
MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。
Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。
Mybatis: 本项目持久层将会使用Mybatis开发。
pagehelper: 分页插件。
spring data redis: 简化java代码操作Redis的API。
5). 工具
git: 版本控制工具, 在团队协作中, 使用该工具对项目中的代码进行管理。
maven: 项目构建工具。
junit:单元测试工具,开发人员功能实现完毕后,需要通过junit对功能进行单元测试。
postman: 接口测工具,模拟用户发起的各类HTTP请求,获取对应的响应结果。
🧠 理论理解
-
技术选型需要根据项目特点选择最合适的技术栈。
-
苍穹外卖选用了:SpringBoot、Vue、小程序、Redis、MySQL、Nginx、Swagger等主流框架和工具。
🏢 企业实战理解
-
字节跳动后端大量使用SpringBoot + MyBatisPlus组合,前端使用Vue2/3,数据缓存大量用Redis Cluster。
-
OpenAI API服务器入口层用Nginx作负载均衡,后端服务以微服务形式部署,数据库多用Cloud Spanner + Redis组合。
3. 开发环境搭建
开发环境搭建主要包含前端环境和后端环境两部分。作为服务端开发工程师, 我们课程学习的重心应该放在后端的业务代码上, 前端的页面我们只需要导入资料中的nginx, 前端页面的代码我们只需要能看懂即可。
3.1 前端环境搭建
1). 前端工程基于 nginx
从资料中找到前端运行环境的nginx,移动到非中文目录下。
sky目录中存放了管理端的前端资源,具体如下:
2). 启动nginx,访问测试
双击 nginx.exe 即可启动 nginx 服务,访问端口号为 80
🧠 理论理解
-
使用Nginx本地搭建前端静态页面环境,使前端可以模拟正式生产环境访问。
-
解压资料中的Nginx,配置前端资源路径即可快速启动。
🏢 企业实战理解
-
企业中前端通常通过独立部署静态资源服务(如阿里用OSS + CDN托管Vue应用)。
-
在腾讯和字节内部开发阶段,开发机用Nginx+Docker容器快速搭建前端环境,支持多人并行调试。
3.2 后端环境搭建
3.2.1 熟悉项目结构
后端工程基于 maven 进行项目构建,并且进行分模块开发。
1). 从当天资料中找到后端初始工程:
2). 用 IDEA 打开初始工程,了解项目的整体结构:
对工程的每个模块作用说明:
序号 | 名称 | 说明 |
---|---|---|
1 | sky-take-out | maven父工程,统一管理依赖版本,聚合其他子模块 |
2 | sky-common | 子模块,存放公共类,例如:工具类、常量类、异常类等 |
3 | sky-pojo | 子模块,存放实体类、VO、DTO等 |
4 | sky-server | 子模块,后端服务,存放配置文件、Controller、Service、Mapper等 |
对项目整体结构了解后,接下来我们详细分析上述的每个子模块:
-
sky-common: 模块中存放的是一些公共类,可以供其他模块使用
分析sky-common模块的每个包的作用:
名称 说明 constant 存放相关常量类 context 存放上下文类 enumeration 项目的枚举类存储 exception 存放自定义异常类 json 处理json转换的类 properties 存放SpringBoot相关的配置属性类 result 返回结果类的封装 utils 常用工具类 -
sky-pojo: 模块中存放的是一些 entity、DTO、VO
分析sky-pojo模块的每个包的作用:
名称 说明 Entity 实体,通常和数据库中的表对应 DTO 数据传输对象,通常用于程序中各层之间传递数据 VO 视图对象,为前端展示数据提供的对象 POJO 普通Java对象,只有属性和对应的getter和setter -
sky-server: 模块中存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等
分析sky-server模块的每个包的作用:
名称 说明 config 存放配置类 controller 存放controller类 interceptor 存放拦截器类 mapper 存放mapper接口 service 存放service类 SkyApplication 启动类
🧠 理论理解
-
后端工程采用Maven多模块分层设计:父工程聚合 + common模块(工具类)+ pojo模块(实体类)+ server模块(业务逻辑)。
🏢 企业实战理解
-
字节跳动内部工程也广泛使用Maven多模块管理,遵循【单一职责】+【松耦合】结构,方便模块独立部署与测试。
3.2.2 Git版本控制
使用Git进行项目代码的版本控制,具体操作:
1). 创建Git本地仓库
当Idea中出现:
说明本地仓库创建成功。
2). 创建Git远程仓库
访问https://gitee.com/,新建仓库
点击 创建
3). 将本地文件推送到Git远程仓库
-
提交文件至本地仓库
忽略以下类型文件
开始提交
中间出现:点击commit
-
添加Git远程仓库地址
复制远程地址:
添加地址:
-
推送
成功推送至远程仓库
3.2.3 数据库环境搭建
-
从资料中找到sky.sql
通过该sql文件直接可创建数据库,所以不需要提前创建数据库,直接导入该文件执行即可。
-
执行sky.sql文件
执行完成后,共创建出11张表
每张表的说明:
序号 | 表名 | 中文名 |
---|---|---|
1 | employee | 员工表 |
2 | category | 分类表 |
3 | dish | 菜品表 |
4 | dish_flavor | 菜品口味表 |
5 | setmeal | 套餐表 |
6 | setmeal_dish | 套餐菜品关系表 |
7 | user | 用户表 |
8 | address_book | 地址表 |
9 | shopping_cart | 购物车表 |
10 | orders | 订单表 |
11 | order_detail | 订单明细表 |
我们目前先简单了解大概有哪些表, 每张表存储什么数据, 有一个印象。对于具体的表结构, 以及表结构中的字段, 可以参考资料中的《数据库设计文档》,同时在讲解具体的功能开发时, 我们也会再详细介绍。
3.2.4 前后端联调
后端的初始工程中已经实现了登录功能,直接进行前后端联调测试即可
实现思路:
1.Controller层
在sky-server模块中,com.sky.controller.admin.EmployeeController
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#aa5500">/**</span>
<span style="color:#aa5500">* 登录</span>
<span style="color:#aa5500">*</span>
<span style="color:#aa5500">* @param employeeLoginDTO</span>
<span style="color:#aa5500">* @return</span>
<span style="color:#aa5500">*/</span>
<span style="color:#555555">@PostMapping</span>(<span style="color:#aa1111">"/login"</span>)
<span style="color:#770088">public</span> <span style="color:#000000">Result</span><span style="color:#981a1a"><</span><span style="color:#000000">EmployeeLoginVO</span><span style="color:#981a1a">></span> <span style="color:#0000ff">login</span>(<span style="color:#555555">@RequestBody</span> <span style="color:#000000">EmployeeLoginDTO</span> <span style="color:#000000">employeeLoginDTO</span>) {
<span style="color:#000000">log</span>.<span style="color:#000000">info</span>(<span style="color:#aa1111">"员工登录:{}"</span>, <span style="color:#000000">employeeLoginDTO</span>);
<span style="color:#aa5500">//调用service方法查询数据库</span>
<span style="color:#000000">Employee</span> <span style="color:#000000">employee</span> <span style="color:#981a1a">=</span> <span style="color:#000000">employeeService</span>.<span style="color:#000000">login</span>(<span style="color:#000000">employeeLoginDTO</span>);
<span style="color:#aa5500">//登录成功后,生成jwt令牌</span>
<span style="color:#000000">Map</span><span style="color:#981a1a"><</span><span style="color:#008855">String</span>, <span style="color:#008855">Object</span><span style="color:#981a1a">></span> <span style="color:#000000">claims</span> <span style="color:#981a1a">=</span> <span style="color:#770088">new</span> <span style="color:#000000">HashMap</span><span style="color:#981a1a"><></span>();
<span style="color:#000000">claims</span>.<span style="color:#000000">put</span>(<span style="color:#000000">JwtClaimsConstant</span>.<span style="color:#000000">EMP_ID</span>, <span style="color:#000000">employee</span>.<span style="color:#000000">getId</span>());
<span style="color:#008855">String</span> <span style="color:#000000">token</span> <span style="color:#981a1a">=</span> <span style="color:#000000">JwtUtil</span>.<span style="color:#000000">createJWT</span>(
<span style="color:#000000">jwtProperties</span>.<span style="color:#000000">getAdminSecretKey</span>(),
<span style="color:#000000">jwtProperties</span>.<span style="color:#000000">getAdminTtl</span>(),
<span style="color:#000000">claims</span>);
<span style="color:#000000">EmployeeLoginVO</span> <span style="color:#000000">employeeLoginVO</span> <span style="color:#981a1a">=</span> <span style="color:#000000">EmployeeLoginVO</span>.<span style="color:#000000">builder</span>()
.<span style="color:#000000">id</span>(<span style="color:#000000">employee</span>.<span style="color:#000000">getId</span>())
.<span style="color:#000000">userName</span>(<span style="color:#000000">employee</span>.<span style="color:#000000">getUsername</span>())
.<span style="color:#000000">name</span>(<span style="color:#000000">employee</span>.<span style="color:#000000">getName</span>())
.<span style="color:#000000">token</span>(<span style="color:#000000">token</span>)
.<span style="color:#000000">build</span>();
<span style="color:#770088">return</span> <span style="color:#000000">Result</span>.<span style="color:#000000">success</span>(<span style="color:#000000">employeeLoginVO</span>);
}</span></span>
2.Service层
在sky-server模块中,com.sky.service.impl.EmployeeServiceImpl
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#aa5500">/**</span>
<span style="color:#aa5500">* 员工登录</span>
<span style="color:#aa5500">*</span>
<span style="color:#aa5500">* @param employeeLoginDTO</span>
<span style="color:#aa5500">* @return</span>
<span style="color:#aa5500">*/</span>
<span style="color:#770088">public</span> <span style="color:#000000">Employee</span> <span style="color:#0000ff">login</span>(<span style="color:#000000">EmployeeLoginDTO</span> <span style="color:#000000">employeeLoginDTO</span>) {
<span style="color:#008855">String</span> <span style="color:#000000">username</span> <span style="color:#981a1a">=</span> <span style="color:#000000">employeeLoginDTO</span>.<span style="color:#000000">getUsername</span>();
<span style="color:#008855">String</span> <span style="color:#000000">password</span> <span style="color:#981a1a">=</span> <span style="color:#000000">employeeLoginDTO</span>.<span style="color:#000000">getPassword</span>();
<span style="color:#aa5500">//1、根据用户名查询数据库中的数据</span>
<span style="color:#000000">Employee</span> <span style="color:#000000">employee</span> <span style="color:#981a1a">=</span> <span style="color:#000000">employeeMapper</span>.<span style="color:#000000">getByUsername</span>(<span style="color:#000000">username</span>);
<span style="color:#aa5500">//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)</span>
<span style="color:#770088">if</span> (<span style="color:#000000">employee</span> <span style="color:#981a1a">==</span> <span style="color:#221199">null</span>) {
<span style="color:#aa5500">//账号不存在</span>
<span style="color:#770088">throw</span> <span style="color:#770088">new</span> <span style="color:#000000">AccountNotFoundException</span>(<span style="color:#000000">MessageConstant</span>.<span style="color:#000000">ACCOUNT_NOT_FOUND</span>);
}
<span style="color:#aa5500">//密码比对</span>
<span style="color:#770088">if</span> (<span style="color:#981a1a">!</span><span style="color:#000000">password</span>.<span style="color:#000000">equals</span>(<span style="color:#000000">employee</span>.<span style="color:#000000">getPassword</span>())) {
<span style="color:#aa5500">//密码错误</span>
<span style="color:#770088">throw</span> <span style="color:#770088">new</span> <span style="color:#000000">PasswordErrorException</span>(<span style="color:#000000">MessageConstant</span>.<span style="color:#000000">PASSWORD_ERROR</span>);
}
<span style="color:#770088">if</span> (<span style="color:#000000">employee</span>.<span style="color:#000000">getStatus</span>() <span style="color:#981a1a">==</span> <span style="color:#000000">StatusConstant</span>.<span style="color:#000000">DISABLE</span>) {
<span style="color:#aa5500">//账号被锁定</span>
<span style="color:#770088">throw</span> <span style="color:#770088">new</span> <span style="color:#000000">AccountLockedException</span>(<span style="color:#000000">MessageConstant</span>.<span style="color:#000000">ACCOUNT_LOCKED</span>);
}
<span style="color:#aa5500">//3、返回实体对象</span>
<span style="color:#770088">return</span> <span style="color:#000000">employee</span>;
}</span></span>
3.Mapper层
在sky-server模块中,com.sky.mapper.EmployeeMapper
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#770088">package</span> <span style="color:#0000ff">com</span>.<span style="color:#000000">sky</span>.<span style="color:#000000">mapper</span>;
<span style="color:#770088">import</span> <span style="color:#000000">com</span>.<span style="color:#000000">sky</span>.<span style="color:#000000">entity</span>.<span style="color:#000000">Employee</span>;
<span style="color:#770088">import</span> <span style="color:#000000">org</span>.<span style="color:#000000">apache</span>.<span style="color:#000000">ibatis</span>.<span style="color:#000000">annotations</span>.<span style="color:#000000">Mapper</span>;
<span style="color:#770088">import</span> <span style="color:#000000">org</span>.<span style="color:#000000">apache</span>.<span style="color:#000000">ibatis</span>.<span style="color:#000000">annotations</span>.<span style="color:#000000">Select</span>;
<span style="color:#555555">@Mapper</span>
<span style="color:#770088">public</span> <span style="color:#770088">interface</span> <span style="color:#0000ff">EmployeeMapper</span> {
<span style="color:#aa5500">/**</span>
<span style="color:#aa5500">* 根据用户名查询员工</span>
<span style="color:#aa5500">* @param username</span>
<span style="color:#aa5500">* @return</span>
<span style="color:#aa5500">*/</span>
<span style="color:#555555">@Select</span>(<span style="color:#aa1111">"select * from employee where username = #{username}"</span>)
<span style="color:#000000">Employee</span> <span style="color:#000000">getByUsername</span>(<span style="color:#008855">String</span> <span style="color:#000000">username</span>);
}
</span></span>
注:可以通过断点调试跟踪后端程序的执行过程
3.2.5 nginx反向代理和负载均衡
对登录功能测试完毕后,接下来,我们思考一个问题:前端发送的请求,是如何请求到后端服务的?
前端请求地址:http://localhost/api/employee/login
后端接口地址:http://localhost:8080/admin/employee/login
前端请求地址 后端接口地址
很明显,两个地址不一致,那是如何请求到后端服务的呢?
1). nginx反向代理
nginx 反向代理,就是将前端发送的动态请求由 nginx 转发到后端服务器
那为什么不直接通过浏览器直接请求后台服务端,需要通过nginx反向代理呢?
nginx 反向代理的好处:
-
提高访问速度
因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。
-
进行负载均衡
所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。
-
保证后端服务安全
因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。
nginx 反向代理的配置方式:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">server</span>{
<span style="color:#ff5500">listen</span> 80;
<span style="color:#ff5500">server_name</span> localhost;
<span style="color:#0055aa">location</span> /api/{
<span style="color:#ff5500">proxy_pass</span> <span style="color:#0055aa">http</span>://localhost:8080/admin/; <span style="color:#aa5500">#反向代理</span>
}
}</span></span>
proxy_pass:该指令是用来设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。
如上代码的含义是:监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。
接下来,进到nginx-1.20.2\conf,打开nginx配置
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#aa5500"># 反向代理,处理管理端发送的请求</span>
<span style="color:#0055aa">location</span> /api/ {
<span style="color:#ff5500">proxy_pass</span> <span style="color:#0055aa">http</span>://localhost:8080/admin/;
<span style="color:#aa5500">#proxy_pass http://webservers/admin/;</span>
}</span></span>
当在访问http://localhost/api/employee/login,nginx接收到请求后转到http://localhost:8080/admin/,故最终的请求地址为http://localhost:8080/admin/employee/login,和后台服务的访问地址一致。
2). nginx 负载均衡
当如果服务以集群的方式进行部署时,那nginx在转发请求到服务器时就需要做相应的负载均衡。其实,负载均衡从本质上来说也是基于反向代理来实现的,最终都是转发请求。
nginx 负载均衡的配置方式:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">upstream</span> webservers{
<span style="color:#0055aa">server</span> 192.168.100.128:8080;
<span style="color:#0055aa">server</span> 192.168.100.129:8080;
}
<span style="color:#0055aa">server</span>{
<span style="color:#ff5500">listen</span> 80;
<span style="color:#ff5500">server_name</span> localhost;
<span style="color:#0055aa">location</span> /api/{
<span style="color:#ff5500">proxy_pass</span> <span style="color:#0055aa">http</span>://webservers/admin;<span style="color:#aa5500">#负载均衡</span>
}
}</span></span>
upstream:如果代理服务器是一组服务器的话,我们可以使用upstream指令配置后端服务器组。
如上代码的含义是:监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://webservers/admin,根据webservers名称找到一组服务器,根据设置的负载均衡策略(默认是轮询)转发到具体的服务器。
注:upstream后面的名称可自定义,但要上下保持一致。
nginx 负载均衡策略:
名称 | 说明 |
---|---|
轮询 | 默认方式 |
weight | 权重方式,默认为1,权重越高,被分配的客户端请求就越多 |
ip_hash | 依据ip分配方式,这样每个访客可以固定访问一个后端服务 |
least_conn | 依据最少连接方式,把请求优先分配给连接数少的后端服务 |
url_hash | 依据url分配方式,这样相同的url会被分配到同一个后端服务 |
fair | 依据响应时间方式,响应时间短的服务将会被优先分配 |
具体配置方式:
轮询:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">upstream</span> webservers{
<span style="color:#0055aa">server</span> 192.168.100.128:8080;
<span style="color:#0055aa">server</span> 192.168.100.129:8080;
}</span></span>
weight:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">upstream</span> webservers{
<span style="color:#0055aa">server</span> 192.168.100.128:<span style="color:#116644">8080 weight</span>=90;
<span style="color:#0055aa">server</span> 192.168.100.129:<span style="color:#116644">8080 weight</span>=10;
}</span></span>
ip_hash:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">upstream</span> webservers{
<span style="color:#770088">ip_hash</span>;
<span style="color:#0055aa">server</span> 192.168.100.128:8080;
<span style="color:#0055aa">server</span> 192.168.100.129:8080;
}</span></span>
least_conn:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">upstream</span> webservers{
least_conn;
<span style="color:#0055aa">server</span> 192.168.100.128:8080;
<span style="color:#0055aa">server</span> 192.168.100.129:8080;
}</span></span>
url_hash:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">upstream</span> webservers{
<span style="color:#000000">hash </span><span style="color:#116644">&request_uri</span>;
<span style="color:#0055aa">server</span> 192.168.100.128:8080;
<span style="color:#0055aa">server</span> 192.168.100.129:8080;
}</span></span>
fair:
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#0055aa">upstream</span> webservers{
<span style="color:#0055aa">server</span> 192.168.100.128:8080;
<span style="color:#0055aa">server</span> 192.168.100.129:8080;
fair;
}</span></span>
3.3 完善登录功能
问题:员工表中的密码是明文存储,安全性太低。
解决思路:
-
将密码加密后存储,提高安全性
-
使用MD5加密方式对明文密码加密
实现步骤:
-
修改数据库中明文密码,改为MD5加密后的密文
打开employee表,修改密码
-
修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对
打开EmployeeServiceImpl.java,修改比对密码
<span style="background-color:#f8f8f8"><span style="color:#aa5500">/**</span> <span style="color:#aa5500">* 员工登录</span> <span style="color:#aa5500">*</span> <span style="color:#aa5500">* @param employeeLoginDTO</span> <span style="color:#aa5500">* @return</span> <span style="color:#aa5500">*/</span> <span style="color:#770088">public</span> <span style="color:#000000">Employee</span> <span style="color:#0000ff">login</span>(<span style="color:#000000">EmployeeLoginDTO</span> <span style="color:#000000">employeeLoginDTO</span>) { <span style="color:#aa5500">//1、根据用户名查询数据库中的数据</span> <span style="color:#aa5500">//2、处理各种异常情况(用户名不存在、密码不对、账号被锁定)</span> <span style="color:#aa5500">//.......</span> <span style="color:#aa5500">//密码比对</span> <span style="color:#aa5500">// TODO 后期需要进行md5加密,然后再进行比对</span> <span style="color:#000000">password</span> <span style="color:#981a1a">=</span> <span style="color:#000000">DigestUtils</span>.<span style="color:#000000">md5DigestAsHex</span>(<span style="color:#000000">password</span>.<span style="color:#000000">getBytes</span>()); <span style="color:#770088">if</span> (<span style="color:#981a1a">!</span><span style="color:#000000">password</span>.<span style="color:#000000">equals</span>(<span style="color:#000000">employee</span>.<span style="color:#000000">getPassword</span>())) { <span style="color:#aa5500">//密码错误</span> <span style="color:#770088">throw</span> <span style="color:#770088">new</span> <span style="color:#000000">PasswordErrorException</span>(<span style="color:#000000">MessageConstant</span>.<span style="color:#000000">PASSWORD_ERROR</span>); } <span style="color:#aa5500">//........</span> <span style="color:#aa5500">//3、返回实体对象</span> <span style="color:#770088">return</span> <span style="color:#000000">employee</span>; }</span>
3.2.2 Git版本控制
🧠 理论理解
-
使用Git进行版本控制,分为本地仓库管理、远程仓库同步,方便多人协作开发。
🏢 企业实战理解
-
腾讯、字节跳动规范使用 GitFlow 工作流。
-
每个功能开发必须开新分支(feature/xxx),开发完必须合并回 develop,并通过MR审核流程(Merge Request)。
3.2.3 数据库环境搭建
🧠 理论理解
-
通过sky.sql一次性导入完整表结构,避免手动建表出错。
-
理解每张表的设计意义(如用户表、订单表、购物车表等)。
🏢 企业实战理解
-
字节跳动实际开发中使用Flyway或Liquibase管理数据库变更脚本,所有数据库更新需记录版本,便于回滚和追溯。
3.2.5 nginx反向代理和负载均衡
🧠 理论理解
-
Nginx作为前端与后端的中间桥梁,通过反向代理转发请求,隐藏后端服务器细节,提高安全性和性能。
-
负载均衡可以分担多台服务器压力,提升系统高可用性。
🏢 企业实战理解
-
京东双11期间,使用多层Nginx做负载均衡,并动态调整权重保障高峰期间访问流畅。
-
Google Cloud负载均衡器内部也大量使用Nginx改进版(Envoy、gRPC proxy)进行请求调度。
4. 导入接口文档
接下来,就要进入到项目的业务开发了,而我们的开发方式就是基本当前企业主流的前后端分离开发方式,那么这种方式就要求我们之前需要先将接口定义好,这样前后端人员才能并行开发,所以,这个章节就需要将接口文档导入到管理平台,为我们后面业务开发做好准备。其实,在真实的企业开发中,接口设计过程其实是一个非常漫长的过程,可能需要多次开会讨论调整,甚至在开发的过程中才会发现某些接口定义还需要再调整,这种情况其实是非常常见的,但是由于项目时间原因,所以选择一次性导入所有的接口,在开发业务功能过程当中,也会带着大家一起来分析一下对应的接口是怎么确定下来的,为什么要这样定义,从而培养同学们的接口设计能力。
🧠 理论理解
-
接口文档(如YApi)是前后端分离项目中重要的协作桥梁,统一定义接口格式、参数、返回值。
🏢 企业实战理解
-
字节跳动内部统一使用 YApi 私有化部署,支持Mock接口、权限管理、接口版本控制。
-
阿里内部大量使用Apifox,做到接口即文档、接口即测试、接口即Mock,提高协作效率。
4.1 前后端分离开发流程
第一步:定义接口,确定接口的路径、请求方式、传入参数、返回参数。
第二步:前端开发人员和后端开发人员并行开发,同时,也可自测。
第三步:前后端人员进行连调测试。
第四步:提交给测试人员进行最终测试。
4.2 操作步骤
将课程资料中提供的项目接口导入YApi。访问地址:http://yapi.smart-xwork.cn/
1). 从资料中找到项目接口文件
2). 导入到YApi平台
在YApi平台创建出两个项目
选择苍穹外卖-管理端接口.json导入
导入成功
另一个用户端json文件也执行相同操作。
5. Swagger
5.1 介绍
Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。 它的主要作用是:
-
使得前后端分离开发更加方便,有利于团队协作
-
接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
-
功能测试
Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
目前,一般都使用knife4j框架。
🧠 理论理解
-
Swagger(Knife4j)可以自动生成后端API接口文档,方便前端联调和后端测试。
🏢 企业实战理解
-
腾讯后台管理系统默认集成Swagger,在开发阶段启用,生产环境关闭接口文档防止泄露。
-
OpenAI在内部服务上也采用接口文档生成工具,确保工程师快速理解服务接口规范。
5.2 使用步骤
-
导入 knife4j 的maven坐标
在pom.xml中添加依赖
<span style="background-color:#f8f8f8"><span style="color:#117700"><</span><span style="color:#117700">dependency</span><span style="color:#117700">></span> <span style="color:#117700"><</span><span style="color:#117700">groupId</span><span style="color:#117700">></span>com.github.xiaoymin<span style="color:#117700"></</span><span style="color:#117700">groupId</span><span style="color:#117700">></span> <span style="color:#117700"><</span><span style="color:#117700">artifactId</span><span style="color:#117700">></span>knife4j-spring-boot-starter<span style="color:#117700"></</span><span style="color:#117700">artifactId</span><span style="color:#117700">></span> <span style="color:#117700"></</span><span style="color:#117700">dependency</span><span style="color:#117700">></span></span>
-
在配置类中加入 knife4j 相关配置
WebMvcConfiguration.java
<span style="background-color:#f8f8f8"><span style="color:#aa5500">/**</span> <span style="color:#aa5500">* 通过knife4j生成接口文档</span> <span style="color:#aa5500">* @return</span> <span style="color:#aa5500">*/</span> <span style="color:#555555">@Bean</span> <span style="color:#770088">public</span> <span style="color:#000000">Docket</span> <span style="color:#0000ff">docket</span>() { <span style="color:#000000">ApiInfo</span> <span style="color:#000000">apiInfo</span> <span style="color:#981a1a">=</span> <span style="color:#770088">new</span> <span style="color:#000000">ApiInfoBuilder</span>() .<span style="color:#000000">title</span>(<span style="color:#aa1111">"苍穹外卖项目接口文档"</span>) .<span style="color:#000000">version</span>(<span style="color:#aa1111">"2.0"</span>) .<span style="color:#000000">description</span>(<span style="color:#aa1111">"苍穹外卖项目接口文档"</span>) .<span style="color:#000000">build</span>(); <span style="color:#000000">Docket</span> <span style="color:#000000">docket</span> <span style="color:#981a1a">=</span> <span style="color:#770088">new</span> <span style="color:#000000">Docket</span>(<span style="color:#000000">DocumentationType</span>.<span style="color:#000000">SWAGGER_2</span>) .<span style="color:#000000">apiInfo</span>(<span style="color:#000000">apiInfo</span>) .<span style="color:#000000">select</span>() .<span style="color:#000000">apis</span>(<span style="color:#000000">RequestHandlerSelectors</span>.<span style="color:#000000">basePackage</span>(<span style="color:#aa1111">"com.sky.controller"</span>)) .<span style="color:#000000">paths</span>(<span style="color:#000000">PathSelectors</span>.<span style="color:#000000">any</span>()) .<span style="color:#000000">build</span>(); <span style="color:#770088">return</span> <span style="color:#000000">docket</span>; }</span>
-
设置静态资源映射,否则接口文档页面无法访问
WebMvcConfiguration.java
<span style="background-color:#f8f8f8"><span style="color:#aa5500">/**</span> <span style="color:#aa5500">* 设置静态资源映射</span> <span style="color:#aa5500">* @param registry</span> <span style="color:#aa5500">*/</span> <span style="color:#770088">protected</span> <span style="color:#008855">void</span> <span style="color:#0000ff">addResourceHandlers</span>(<span style="color:#000000">ResourceHandlerRegistry</span> <span style="color:#000000">registry</span>) { <span style="color:#000000">registry</span>.<span style="color:#000000">addResourceHandler</span>(<span style="color:#aa1111">"/doc.html"</span>).<span style="color:#000000">addResourceLocations</span>(<span style="color:#aa1111">"classpath:/META-INF/resources/"</span>); <span style="color:#000000">registry</span>.<span style="color:#000000">addResourceHandler</span>(<span style="color:#aa1111">"/webjars/**"</span>).<span style="color:#000000">addResourceLocations</span>(<span style="color:#aa1111">"classpath:/META-INF/resources/webjars/"</span>); }</span>
-
访问测试
接口文档访问路径为 http://ip:port/doc.html ---> http://localhost:8080/doc.html
接口测试:测试登录功能
思考:通过 Swagger 就可以生成接口文档,那么我们就不需要 Yapi 了?
1、Yapi 是设计阶段使用的工具,管理和维护接口
2、Swagger 在开发阶段使用的框架,帮助后端开发人员做后端的接口测试
🧠 理论理解
-
通过Docket配置生成文档,通过注解完善接口说明,如@Api、@ApiOperation、@ApiModel等。
🏢 企业实战理解
-
字节跳动规范要求每个API接口必须使用@Api + @ApiOperation描述清楚,并统一返回Result对象封装,便于自动化测试和接口一致性校验。
5.3 常用注解
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
注解 | 说明 |
---|---|
@Api | 用在类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,例如entity、DTO、VO |
@ApiModelProperty | 用在属性上,描述属性信息 |
@ApiOperation | 用在方法上,例如Controller的方法,说明方法的用途、作用 |
接下来,使用上述注解,生成可读性更好的接口文档
在sky-pojo模块中
EmployeeLoginDTO.java
<span style="background-color:#f8f8f8"><span style="color:#333333"><span style="color:#770088">package</span> <span style="color:#0000ff">com</span>.<span style="color:#000000">sky</span>.<span style="color:#000000">dto</span>;
<span style="color:#770088">import</span> <span style="color:#000000">io</span>.<span style="color:#000000">swagger</span>.<span style="color:#000000">annotations</span>.<span style="color:#000000">ApiModel</span>;
<span style="color:#770088">import</span> <span style="color:#000000">io</span>.<span style="color:#000000">swagger</span>.<span style="color:#000000">annotations</span>.<span style="color:#000000">ApiModelProperty</span>;
<span style="color:#770088">import</span> <span style="color:#000000">lombok</span>.<span style="color:#000000">Data</span>;
<span style="color:#770088">import</span> <span style="color:#000000">java</span>.<span style="color:#000000">io</span>.<span style="color:#000000">Serializable</span>;
<span style="color:#555555">@Data</span>
<span style="color:#555555">@ApiModel</span>(<span style="color:#000000">description</span> <span style="color:#981a1a">=</span> <span style="color:#aa1111">"员工登录时传递的数据模型"</span>)
<span style="color:#770088">public</span> <span style="color:#770088">class</span> <span style="color:#0000ff">EmployeeLoginDTO</span> <span style="color:#770088">implements</span> <span style="color:#000000">Serializable</span> {
<span style="color:#555555">@ApiModelProperty</span>(<span style="color:#aa1111">"用户名"</span>)
<span style="color:#770088">private</span> <span style="color:#008855">String</span> <span style="color:#000000">username</span>;
<span style="color:#555555">@ApiModelProperty</span>(<span style="color:#aa1111">"密码"</span>)
<span style="color:#770088">private</span> <span style="color:#008855">String</span> <span style="color:#000000">password</span>;
}
</span></span>
EmployeeLoginVo.java
<span style="background-color:#f8f8f8"><span style="color:#333333">package com.sky.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {
@ApiModelProperty("主键值")
private Long id;
@ApiModelProperty("用户名")
private String userName;
@ApiModelProperty("姓名")
private String name;
@ApiModelProperty("jwt令牌")
private String token;
}</span></span>
在sky-server模块中
EmployeeController.java
<span style="background-color:#f8f8f8"><span style="color:#333333">package com.sky.controller.admin;
import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.entity.Employee;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.EmployeeService;
import com.sky.utils.JwtUtil;
import com.sky.vo.EmployeeLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 员工管理
*/
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
//..............
}
/**
* 退出
*
* @return
*/
@PostMapping("/logout")
@ApiOperation("员工退出")
public Result<String> logout() {
return Result.success();
}
}
</span></span>
启动服务:访问http://localhost:8080/doc.html