# 苍穹外卖 ## 项目简介 ### 整体介绍 本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 **系统管理后台** 和 **小程序端应用** 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。 ![image-20221106194424735](https://pic.bitday.top/i/2025/04/10/qipsdw-0.png) **1). 管理端功能** 员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 ,数据统计,来单提醒。 **2). 用户端功能** 微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 支付、分类及菜品浏览。 ### 技术选型 ![image-20221106185646994](https://pic.bitday.top/i/2025/04/10/qk8jfn-0.png) **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 run 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 ``` #### 后端环境搭建 ![image-20250410161050451](https://pic.bitday.top/i/2025/04/10/qmunln-0.png) 工程的每个模块作用说明: | **序号** | **名称** | **说明** | | -------- | ------------ | ------------------------------------------------------------ | | 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 | 订单明细表 | #### 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)。 **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.易用性与团队协作 界面直观、操作便捷,支持多人协作,通过分支管理和版本控制,团队成员可以并行开发并进行变更管理,确保接口维护有序。 **迭代分支功能:** 新建迭代分支,新增的待测试的接口在这里充分测试,没问题之后合并回主分支。 image-20250410180705866 **导出接口文档:** 推荐导出数据格式为OpenAPI Spec,它是一种通用的 API 描述标准,Postman和APIFox都支持。 image-20250410182110385 #### 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; } ``` ![image-20240327170247852](https://pic.bitday.top/i/2025/03/19/u7ybj9-2.png) 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文件中。 **最后项目结构:** ![image-20250515092129486](https://pic.bitday.top/i/2025/05/15/f8ix5c-0.png) ### 滚动开发阶段 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 ![image-20250515111352735](https://pic.bitday.top/i/2025/05/15/iexngr-0.png) 注意把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 ``` ## 实战开发 ### 分页查询 传统员工分页查询分析: ![image-20221215153413290](https://pic.bitday.top/i/2025/03/19/u6p5ae-2.png) **采用分页插件PageHelper:** ![image-20221215170038833](https://pic.bitday.top/i/2025/03/19/u6o1ho-2.png) **在执行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); } } ``` ### 条件分页查询 思路分析: ![image-20221215180528415](https://pic.bitday.top/i/2025/03/19/u6mjck-2.png) ```java