# 苍穹外卖
## 项目简介
### 整体介绍
本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 **系统管理后台** 和 **小程序端应用** 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。

**1). 管理端功能**
员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 ,数据统计,来单提醒。
**2). 用户端功能**
微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 支付、分类及菜品浏览。
### 技术选型

**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请求,获取对应的响应结果。
### 准备工作
//待完善,最后写一套本地java开发、nginx部署前端;服务器docker部署的方案!!!
#### 前端环境搭建
**Windows下**
1.构建和打包前端项目
```bash
npm install #安装依赖
npm run build #打包,但我TypeScript 检查报错
npm run pure-build #不检查类型打包
```
2.将构建文件复制到指定目录
Nginx 默认的静态文件根目录通常是 **`/usr/share/nginx/html`**,你可以选择将打包好的静态文件拷贝到该目录
或者使用自定义目录`/var/www/my-frontend`,并修改 Nginx 配置文件来指向这个目录。
3.配置 Nginx
打开 Nginx 的配置文件,通常位于 `/etc/nginx/nginx.conf`
以下是一个使用自定义目录 `/var/www/my-frontend` 作为站点根目录的示例配置:
```nginx
server {
listen 80;
server_name your-domain.com; # 如果没有域名可以使用 _ 或 localhost
root /var/www/my-frontend;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
```
4.启动或重启 Nginx
启动:双击nginx.exe
重启:nginx -s reload
5.查看是否正在运行
```bash
tasklist /FI "IMAGENAME eq nginx.exe"
```
6.访问前端项目
在浏览器中输入你配置的域名或服务器 IP 地址
终止运行nginx:
```bash
nginx.exe -s stop
```
#### 后端环境搭建

工程的每个模块作用说明:
| **序号** | **名称** | **说明** |
| -------- | ------------ | ------------------------------------------------------------ |
| 1 | sky-take-out | maven父工程,统一管理依赖版本,聚合其他子模块 |
| 2 | sky-common | 子模块,存放公共类,例如:工具类、常量类、异常类等 |
| 3 | sky-pojo | 子模块,存放实体类、VO、DTO等 |
| 4 | sky-server | 子模块,后端服务,存放配置文件、Controller、Service、Mapper等 |
分析sky-common模块的每个包的作用:
| 名称 | 说明 |
| ----------- | ------------------------------ |
| constant | 存放相关常量类 |
| context | 存放上下文类 |
| enumeration | 项目的枚举类存储 |
| exception | 存放自定义异常类 |
| json | 处理json转换的类 |
| properties | 存放SpringBoot相关的配置属性类 |
| result | 返回结果类的封装 |
| utils | 常用工具类 |
分析sky-pojo模块的每个包的作用:
| **名称** | **说明** |
| -------- | ------------------------------------------------------------ |
| Entity | 实体,通常和数据库中的表对应 |
| DTO | 数据传输对象,通常用于程序中各层之间传递数据(接收从web来的数据) |
| VO | 视图对象,为前端展示数据提供的对象(响应给web) |
| POJO | 普通Java对象,只有属性和对应的getter和setter |
分析sky-server模块的每个包的作用:
| 名称 | 说明 |
| -------------- | ---------------- |
| config | 存放配置类 |
| controller | 存放controller类 |
| interceptor | 存放拦截器类 |
| mapper | 存放mapper接口 |
| service | 存放service类 |
| SkyApplication | 启动类 |
#### 数据库初始化
执行sky.sql文件
| 序号 | 数据表名 | 中文名称 |
| ---- | ------------- | -------------- |
| 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 | 订单明细表 |
#### **跨域问题产生方式:**
- 你在地址栏输 `http://localhost:8100/api/health`,这是浏览器直接导航到该 URL——浏览器会绕过 CORS 机制,直接向服务器发请求并渲染响应结果,这种场景不受 CORS 限制。
- 但如果你在一个在 `http://localhost:3000`(或任何不同端口)下运行的前端网页里,用 JavaScript 这样写:
```js
fetch('http://localhost:8100/api/health')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
```
会触发跨域
#### Nginx
**1.静态资源托管**
直接高效地托管前端静态文件(HTML/CSS/JS/图片等)。
```nginx
server {
root /var/www/html;
index index.html;
location / {
try_files $uri $uri/ /index.html; # 支持前端路由(如 React/Vue)
}
}
```
- **`try_files`**:按顺序尝试多个文件或路径,直到找到第一个可用的为止。
- `$uri`:尝试直接访问请求的路径对应的文件(例如 `/css/style.css`)。
- `$uri/`:尝试将路径视为目录(例如 `/blog/` 会查找 `/blog/index.html`)。
- `/index.html`:如果前两者均未找到,最终返回前端入口文件 `index.html`。
**2.nginx 反向代理:**
**反向代理的好处:**
- 提高访问速度
因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。
- 保证后端服务安全
因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。
- 统一入口**解决跨域**问题(无需后端配置 CORS)。
```java
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 覆盖所有请求
registry.addMapping("/**")
// 允许发送 Cookie
.allowCredentials(true)
// 放行哪些域名(必须用 patterns,否则 * 会和 allowCredentials 冲突)
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.exposedHeaders("*");
}
}
```
**nginx 反向代理的配置方式:**
```nginx
server{
listen 80;
server_name localhost;
location /api/{
proxy_pass http://localhost:8080/admin/; #反向代理
}
}
```
监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。
**3.负载均衡配置**(默认是轮询)
将流量分发到多个后端服务器,提升系统吞吐量和容错能力。
```nginx
upstream webservers{
server 192.168.100.128:8080;
server 192.168.100.129:8080;
}
server{
listen 80;
server_name localhost;
location /api/{
proxy_pass http://webservers/admin;#负载均衡
}
}
```
**完整流程示例**
1. **用户访问**:浏览器打开 `http://yourdomain.com`。
2. **Nginx 返回静态文件**:返回 `index.html` 和前端资源。
3. **前端发起 API 请求**:前端代码调用 `/api/data`。
4. **Nginx 代理请求**:将 `/api/data` 转发到 `http://backend_server:3000/api/data`。
5. **后端响应**:处理请求并返回数据,Nginx 将结果传回前端。
#### APIFox
使用APIFox管理、测试接口、导出接口文档...
**优势:**
1.多格式支持
APIFox 能够导入包括 YApi 格式在内的多种接口文档,同时支持导出为 OpenAPI、Postman Collection、Markdown 和 HTML 等格式,使得接口文档在不同工具间无缝迁移和使用。
2.接口调试与 Mock
内置接口调试工具可以直接发送请求、查看返回结果,同时内置 Mock 服务功能,方便前后端联调和接口数据模拟,提升开发效率。
3.易用性与团队协作
界面直观、操作便捷,支持多人协作,通过分支管理和版本控制,团队成员可以并行开发并进行变更管理,确保接口维护有序。
**迭代分支功能:**
新建迭代分支,新增的待测试的接口在这里充分测试,没问题之后合并回主分支。
**导出接口文档:**
推荐导出数据格式为OpenAPI Spec,它是一种通用的 API 描述标准,Postman和APIFox都支持。
#### Swagger
1. 使得前后端分离开发更加方便,有利于团队协作
2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担
3. 功能测试
**使用:**
**1.导入 knife4j 的maven坐标**
在pom.xml中添加依赖
```java
com.github.xiaoymin
knife4j-spring-boot-starter
```
**2.在配置类中加入 knife4j 相关配置**
WebMvcConfiguration.java
```java
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any())
.build();
return docket;
}
```
**3.设置静态资源映射,否则接口文档页面无法访问**
WebMvcConfiguration.java
```java
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
```
**4.访问测试**
接口文档访问路径为 http://ip:port/doc.html ---> http://localhost:8080/doc.html
这是根据后端 Java 代码(通常是注解)自动生成接口文档,访问是通过**后端服务的端口**,这些文档最终会以静态文件的形式存在于 jar 包内,通常存放在 `META-INF/resources/`
**常用注解**
通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:
| **注解** | **说明** |
| ----------------- | ---------------------------------------------------------- |
| @Api | 用在类上,例如Controller,表示对类的说明 |
| @ApiModel | 用在类上,例如entity、DTO、VO |
| @ApiModelProperty | 用在属性上,描述属性信息 |
| @ApiOperation | 用在**方法**上,例如Controller的方法,说明方法的用途、作用 |
EmployeeLoginDTO.java
```java
@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {
@ApiModelProperty("用户名")
private String username;
@ApiModelProperty("密码")
private String password;
}
```

EmployeeController.java
```java
@Api(tags = "员工相关接口")
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@Autowired
private JwtProperties jwtProperties;
/**
* 登录
*
* @param employeeLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
//..............
}
}
```
## 后端部署
### 项目开发完毕
这种情况下JAVA代码无需改动,直接本地打包maven->package成Jar包复制到服务器上部署:
本项目为multi-module (聚合) Maven 工程,父工程为sky-take-out,子模块有common,pojo,server,其中server依赖common和pojo:
```xml
com.sky
sky-common
1.0-SNAPSHOT
com.sky
sky-pojo
1.0-SNAPSHOT
```
**打包方式:**
1.直接对父工程执行mvn clean install
2.分别对子模块common和pojo执行install,再对server执行package
因为Maven 在构建 `sky-server` 时,去你本地仓库或远程仓库寻找它依赖的两个 SNAPSHOT 包。
在父工程的pom中添加这段,能将你的应用和所有依赖都打到一个可执行的 “fat jar” 里
```xml
${project.artifactId}
org.springframework.boot
spring-boot-maven-plugin
```
**JAVA项目dockerfile:**
```dockerfile
# 使用 JDK 17 运行时镜像
FROM openjdk:17-jdk-slim
# 设置时区为上海
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
# 创建工作目录
WORKDIR /app
# 复制 Fat Jar,重命名为 app.jar
COPY sky-server-1.0-SNAPSHOT.jar ./app.jar
# 暴露端口(与 application.properties 中的 server.port 保持一致)
EXPOSE 8085
# 以 exec 形式启动
ENTRYPOINT ["java", "-jar", "app.jar"]
```
`ENTRYPOINT ["java", "-jar", "app.jar"]` 能够启动是因为Spring Boot 的 Maven 插件在打包时已经将你的启动类标记进去了:
Spring Boot 的启动器会:
1. 读取 `MANIFEST.MF` 里的 `Start-Class: com.sky.SkyApplication`
2. 将 `com.sky.SkyApplication` 作为入口调用其 `main` 方法
由于该项目还需依赖Mysql和Redis运行,因此需在**docker-compose.yml**中统一创建容器环境。(:ro代表只读)
```yml
version: "3.8"
services:
mysql:
image: mysql:8.0
container_name: sky-mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: sky_take_out
TZ: Asia/Shanghai
volumes:
- ./data/mysql:/var/lib/mysql
- ./init/sky.sql:/docker-entrypoint-initdb.d/sky.sql:ro
- ./mysql-conf/my.cnf:/etc/mysql/conf.d/my.cnf:ro
ports:
- "3306:3306"
networks:
- sky-net
redis:
image: redis:7.0-alpine
container_name: sky-redis
restart: always
command: redis-server --requirepass 123456
volumes:
- ./data/redis:/data
ports:
- "6379:6379"
networks:
- sky-net
app:
build:
context: .
dockerfile: Dockerfile
image: sky-server:latest
container_name: sky-server
depends_on:
- mysql
- redis
volumes:
- ./config:/app/config:ro
environment:
TZ: Asia/Shanghai
SPRING_PROFILES_ACTIVE: dev
ports:
- "8085:8085"
restart: always
networks:
- sky-net
volumes:
mysql:
redis:
networks:
sky-net:
external: true
```
**其中启动数据库要准备两份文件:**
初始化脚本sky.sql,用来创建数据库和表
my.cnf:让初始化脚本创建的表中的中文数据正常显示
```cnf
[client]
default-character-set = utf8mb4
[mysql]
default-character-set = utf8mb4
[mysqld]
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
init_connect='SET NAMES utf8mb4'
```
**另外:**
`application-dev.yml` 是给 **Spring Boot** 读取的,Spring Boot 会在启动时自动加载它,填充到JAVA项目中。
Docker Compose 里的 `environment:` 无法读取`application-dev.yml`,要不就写死、要不就写在.env文件中。
**最后项目结构:**

### 滚动开发阶段
1.仅需改动Dokcerfile,docker-compose无需更改:
```dockerfile
# —— 第一阶段:Maven 构建 ——
FROM maven:3.8.7-eclipse-temurin-17-alpine AS builder
WORKDIR /workspace
# 把项目级 settings.xml 复制到容器里
COPY .mvn/settings.xml /root/.m2/settings.xml
# 1) 先把父 POM 和所有子模块的目录结构都复制过来
COPY whut-take-out-backend/pom.xml ./pom.xml
COPY whut-take-out-backend/sky-common ./sky-common
COPY whut-take-out-backend/sky-pojo ./sky-pojo
COPY whut-take-out-backend/sky-server ./sky-server
# (可选:如果父 pom 有 ,也可把 settings.xml、父级的其它 POM 拷过来)
RUN mvn dependency:go-offline -B
# 2) 拷贝所有子模块源码
COPY whut-take-out-backend ./whut-take-out-backend
# 3) 只构建 sky-server 模块(并且把依赖模块一并编译)
RUN mvn -f whut-take-out-backend/pom.xml clean package \
-pl sky-server -am \
-DskipTests -B
# —— 第二阶段:运行时镜像 ——
FROM openjdk:17-jdk-slim
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
WORKDIR /app
# 4) 把第一阶段产物(sky-server 模块的 Jar)拷过来
COPY --from=builder \
/workspace/whut-take-out-backend/sky-server/target/sky-server-*.jar \
./app.jar
EXPOSE 8085
ENTRYPOINT ["java", "-jar", "app.jar"]
```
2.maven构建依赖可能比较慢,需创建.mvn/settings.xml
```xml
aliyun
aliyun maven
https://maven.aliyun.com/repository/public
central,apache.snapshots
```
使用阿里云镜像加速
3.验证:http://124.71.159.195:8085/doc.html
## 前端部署
直接部署开发完毕的前端代码,准备:
0.创建docker网络:`docker network create sky-net`
1.静态资源html文件夹(npm run build 打包源码获得)
2.nginx.conf

注意把nginx.conf中的server改为Docker 容器(或服务)在**同一网络中**的主机名,如
```nginx
upstream webservers {
server sky-server:8085 weight=90;
}
```
因为同一个网络下的服务名会自动注册DNS,进行地址解析!
3.docker-compose文件
```yml
version: "3.8"
services:
frontend:
image: nginx:alpine
container_name: sky-frontend
ports:
- "85:80"
volumes:
# 把本地 html 目录挂到容器的默认站点目录
- ./html:/usr/share/nginx/html:ro
# 把本地的 nginx.conf 覆盖容器里的配置
- ./nginx.conf:/etc/nginx/nginx.conf:ro
networks:
- sky-net
networks:
sky-net:
external: true
```
## 实战开发
### 分页查询
传统员工分页查询分析:

**采用分页插件PageHelper:**

**在执行empMapper.list()方法时,就是执行:select * from emp 语句,怎么能够实现分页操作呢?**
分页插件帮我们完成了以下操作:
1. 先获取到要执行的SQL语句:
```
select * from emp
```
2. 为了实现分页,第一步是获取符合条件的总记录数。分页插件将原始 SQL 查询中的 `SELECT *` 改成 `SELECT count(*)`
```mysql
select count(*) from emp;
```
3. 一旦知道了总记录数,分页插件会将 `SELECT *` 的查询语句进行修改,加入 `LIMIT` 关键字,限制返回的记录数。
```mysql
select * from emp limit ?, ?
```
第一个参数(`?`)是 起始位置,通常是 `(当前页 - 1) * 每页显示的记录数`,即从哪一行开始查询。
第二个参数(`?`)是 每页显示的记录数,即返回多少条数据。
4. 执行分页查询,例如,假设每页显示 10 条记录,你请求第 2 页数据,那么 SQL 语句会变成:
```mysql
select * from emp limit 10, 10;
```
**使用方法:**
当使用 **PageHelper** 分页插件时,无需在 Mapper 中手动处理分页。只需在 Mapper 中编写常规的列表查询。
- 在 **Service 层**,调用 Mapper 方法之前,**设置分页参数**。
- 调用 Mapper 查询后,**自动进行分页**,并将结果封装到 `PageBean` 对象中返回。
1、在pom.xml引入依赖
```java
com.github.pagehelper
pagehelper-spring-boot-starter
1.4.2
```
2、EmpMapper
```java
@Mapper
public interface EmpMapper {
//获取当前页的结果列表
@Select("select * from emp")
public List list();
}
```
3、EmpServiceImpl
当调用 `PageHelper.startPage(page, pageSize)` 时,PageHelper 插件会拦截随后的 SQL 查询,自动修改查询,加入 `LIMIT` 子句来实现分页功能。
```java
@Override
public PageBean page(Integer page, Integer pageSize) {
// 设置分页参数
PageHelper.startPage(page, pageSize); //page是页号,不是起始索引
// 执行分页查询
List empList = empMapper.list();
// 获取分页结果
Page p = (Page) empList;
//封装PageBean
PageBean pageBean = new PageBean(p.getTotal(), p.getResult());
return pageBean;
}
```
4、Controller
```java
@Slf4j
@RestController
@RequestMapping("/emps")
public class EmpController {
@Autowired
private EmpService empService;
//条件分页查询
@GetMapping
public Result page(@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer pageSize) {
//记录日志
log.info("分页查询,参数:{},{}", page, pageSize);
//调用业务层分页查询功能
PageBean pageBean = empService.page(page, pageSize);
//响应
return Result.success(pageBean);
}
}
```
### 条件分页查询
思路分析:

```java