苍穹外卖总结

前言

1、软件开发流程

瀑布模型
    
    需求分析//需求规格说明书、产品原型
      ↓  
     设计   //UI设计、数据库设计、接口设计
      ↓
     编码   //项目代码、单元测试
      ↓ 
     测试   //测试用例、测试报告
      ↓
    上线运维  //软件环境安装、配置
    
第一阶段:需求分析
    需求规格说明书、产品原型
    一般来说就是使用 **Word** 文档来描述当前项目的各个组成部分,如:系统定义、应用环境、功能规格、性能需求等,都会在文档中描述
    产品原型,一般是使用**Axure RP**软件设计。
    
第二阶段: 设计
    设计的内容包含 UI设计、数据库设计、接口设计。
    
    UI设计:用户界面的设计,主要设计项目的页面效果,小到一个按钮,大到一个页面布局,还有人机交互逻辑的体现。
    
    数据库设计:使用**PowerDesigner**设计当前项目中涉及到哪些数据库,每一个数据库里面包含哪些表,这些表结构之间的关系是什么样的,表结构中包含哪些字段。
    
    接口设计:通过分析原型图,首先,粗粒度地分析每个页面有多少接口,然后,再细粒度地分析每个接口的传入参数,返回值参数,同时明确接口路径及请求方式。
 
第三阶段: 编码
    编写项目代码、并完成单元测试,接口测试。

    项目代码编写:作为软件开发工程师,我们需要对项目的模块功能分析后,进行编码实现。

    单元测试:编码实现完毕后,进行单元测试,单元测试通过后再进入到下一阶段

第四阶段: 测试
    
    在该阶段中主要由测试人员, 对部署在测试环境的项目进行功能测试, 并出具测试报告。
    
第五阶段: 上线运维
    
    在项目上线之前, 会由运维人员准备服务器上的软件环境安装、配置, 配置完毕后, 再将我们开发好的项目,部署在服务器上运行。

2、角色分工

岗位/角色对应阶段职责/分工
项目经理PM(project manager)全阶段对整个项目负责,任务分配、把控进度
产品经理需求分析进行需求调研,输出需求调研文档、产品原型等
UI设计师设计根据产品原型输出界面效果图
架构师,资深工程师设计项目整体架构设计、技术选型等
开发工程师编码功能代码实现
测试工程师测试编写测试用例,输出测试报告
运维工程师上线运维软件环境搭建、项目上线

3、软件环境

1). 开发环境dev(development)

我们作为软件开发人员,在开发阶段使用的环境,就是开发环境,一般外部用户无法访问。

2). 测试环境test(testing)

当软件开发工程师,将项目的功能模块开发完毕,并且单元测试通过后,就需要将项目部署到测试服务器上,让测试人员对项目进行测试。那这台测试服务器就是专门给测试人员使用的环境, 也就是测试环境,用于项目测试,一般外部用户无法访问。

3). 生产环境prod(production)

    当项目开发完毕,并且由测试人员测试通过之后,就可以上线项目,将项目部署到线上环境,并正式对外提供服务,这个线上环境也称之为生产环境。
    首先,会在开发环境中进行项目开发,往往开发环境大多数都是本地的电脑环境和局域网内的环境,当开发完毕后,然后会把项目部署到测试环境,测试环境一般是一台独立测试服务器的环境,项目测试通过后,最终把项目部署到生产环境,生产环境可以是机房或者云服务器等线上环境。

一、苍穹外卖

1.项目介绍

苍穹外卖 { 管理端 { 员工管理 分类管理 菜品管理 套餐管理 订单管理 工作台 数据统计 来电提醒 用户端 { 微信登录 商品浏览 购物车 用户下单 微信支付 历史订单 地址管理 用户催单 苍穹外卖 \begin{cases} 管理端 &\begin{cases}员工管理\\ 分类管理\\ 菜品管理\\ 套餐管理\\ 订单管理\\ 工作台\\ 数据统计\\ 来电提醒 \end{cases}\\ \\ 用户端 &\begin{cases}微信登录\\ 商品浏览\\ 购物车\\ 用户下单\\ 微信支付\\ 历史订单\\ 地址管理\\ 用户催单 \end{cases} \end{cases} 苍穹外卖 管理端用户端 员工管理分类管理菜品管理套餐管理订单管理工作台数据统计来电提醒 微信登录商品浏览购物车用户下单微信支付历史订单地址管理用户催单

1). 管理端

餐饮企业内部员工使用。 主要功能有:

模块描述
登录/退出内部员工必须登录后,才可以访问系统管理后台
员工管理管理员可以在系统后台对员工信息进行管理,包含查询、新增、编辑、禁用等功能
分类管理主要对当前餐厅经营的 菜品分类 或 套餐分类 进行管理维护, 包含查询、新增、修改、删除等功能
菜品管理主要维护各个分类下的菜品信息,包含查询、新增、修改、删除、启售、停售等功能
套餐管理主要维护当前餐厅中的套餐信息,包含查询、新增、修改、删除、启售、停售等功能
订单管理主要维护用户在移动端下的订单信息,包含查询、取消、派送、完成,以及订单报表下载等功能
数据统计主要完成对餐厅的各类数据统计,如营业额、用户数量、订单等

2). 用户端

移动端应用主要提供给消费者使用。主要功能有:

模块描述
登录/退出用户需要通过微信授权后登录使用小程序进行点餐
点餐-菜单在点餐界面需要展示出菜品分类/套餐分类, 并根据当前选择的分类加载其中的菜品信息, 供用户查询选择
点餐-购物车用户选中的菜品就会加入用户的购物车, 主要包含 查询购物车、加入购物车、删除购物车、清空购物车等功能
订单支付用户选完菜品/套餐后, 可以对购物车菜品进行结算支付, 这时就需要进行订单的支付
个人信息在个人中心页面中会展示当前用户的基本信息, 用户可以管理收货地址, 也可以查询历史订单数据

2、技术选型

    关于本项目的技术选型, 我们将会从 用户层、网关层、应用层、数据层 这几个方面进行介绍,主要用于展示项目中使用到的技术框架和中间件等。
苍穹外卖
用户层
网管层
node.js
VUE.js
ElementUI
微信小程序
apache echarts
Nginx
苍穹外卖
应用层
数据层
Spring Boot
Spring MVC
Spring Task
httpclient
Spring Cache
JWT
阿里云OSS
Swagger
POI
WebSocket
MySQL
Redis
mybatis
pagehelper
spring Data rdis

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请求,获取对应的响应结果。

3、开发环境搭建

{ 前端 { 管理端( W e b ) 用户端 ( 小程序 ) 后端 后端服务 ( J A V A ) \begin{cases}前端 &\begin{cases}管理端(Web)\\用户端(小程序) \end{cases} \\\\ 后端 & 后端服务(JAVA) \end{cases} 前端后端{管理端(Web用户端(小程序)后端服务(JAVA)

开发环境搭建主要包含前端环境后端环境两部分。

1) .前端环境搭建

 前端工程基于 nginx,为了避免之前启动的nginx占用了80端口,可关掉之前启动的其他nginx

2).后端环境搭建

分析sky-common模块的每个包的作用:

名称说明
constant存放相关常量类
context存放上下文类
enumeration项目的枚举类存储
exception存放自定义异常类
json处理json转换的类
properties存放SpringBoot相关的配置属性类
result返回结果类的封装
utils常用工具类

分析sky-server模块的每个包的作用:

名称说明
config存放配置类
controller存放controller类
interceptor存放拦截器类
mapper存放mapper接口
service存放service类
SkyApplication启动类

3)Git版本控制

   使用Git进行项目代码的版本控制,创建Git远程,访问https://gitee.com/projects/new,新建仓库:sky-take-out,3). 将本地文件推送到Git远程仓库,提交文件至本地仓库,添加Git远程仓库地址,进行推送,成功推送至远程仓库。

4)数据库环境搭建

在数据库中创建如下表格

序号表名中文名
1employee员工表
2category分类表
3dish菜品表
4dish_flavor菜品口味表
5setmeal套餐表
6setmeal_dish套餐菜品关系表
7user用户表
8address_book地址表
9shopping_cart购物车表
10orders订单表
11order_detail订单明细表
  建表规范:(阿里开源的Java开发手册(黄山版).pdf)
- 【强制】表名、字段名必须使用小写字母或数字,禁止出现数字开头禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
  说明:MySQL 在 Windows 下不区分大小写,但在 Linux 下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
  正例:aliyun_admin,rdc_config,level3_name
  反例:AliyunAdmin,rdcConfig,level_3_name
- 【强制】表名不使用复数名词。
  说明:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于 DO 类名也是单数形式,符合表达习惯。
- 【强制】禁用保留字,如 order, by, desc、range等,请参考 MySQL 官方保留字。

5).前后端联调

1)准备工作:

- 确认MySQL密码,打开application.yml会看到配置如下:
spring:
  datasource:
    druid:
      driver-class-name: ${sky.datasource.driver-class-name}
      username: ${sky.datasource.username}
      password: ${sky.datasource.password}
发现使用的是${...}:作用其实和注解@Value("${user.age}")一样,都是从配置文件中读取配置.

#启动之前,记得确认数据库配置:application-dev.yml
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    #改成自己的数据库密码
    password: root
编译父工程sky-take-out,保证依赖的所有jar包都下载成功

启动SkyApplication,直接进行前后端联调测试即可:http://localhost
Tomcat started on port(s): 8080 (http) with context path ''
com.sky.SkyApplication: Started SkyApplication in 4.585 seconds (JVM running for 5.475)
com.sky.SkyApplication: server started

# 3)后端的初始工程中已经实现了**登录**功能,通过debug查看执行流程:

4、JWT令牌技术

JWT全称:JSON Web Token  (官网:https://jwt.io/)

- 定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。

自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。

简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。

- 第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

- 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}

- 第三部分:Signature(签名),防止Token被篡改确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来:HS256(header+payload, secret)

Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用
的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,
最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号.

需要注意的是Base64是编码方式,而不是加密方式。


JWT令牌最典型的应用场景就是登录认证:

1. 在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成
 的 jwt令牌返回给前端。
2. 前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。
3. 服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过
来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。


在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:

1. 在登录成功之后,要生成令牌。
2. 每一次请求当中,要接收令牌并对令牌进行校验。

5、Nginx反向代理和负载均衡

1)、nginx反向代理

前端发送的请求,是如何请求到后端服务的?很明显,前端请求和后端接口两个地址不一致,那是如何请求到后端服务的呢?

**nginx 反向代理**,就是将前端发送的**动态请求**由 nginx 转发到后端服务器。

**nginx 反向代理的好处:**
- 提高访问速度
  因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。

- 进行负载均衡
  所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。

- 保证后端服务安全
  因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。

nginx 反向代理的配置方式: 

server{
    listen 80;  #监听80端口号,
    server_name localhost;
    
    # 反向代理,处理管理端发送的请求
    location /api/{
        proxy_pass http://localhost:8080/admin/; #反向代理
    }
}
# **proxy_pass:**该指令是用来设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。

打开nginx配置: nginx.conf

当在访问http://localhost/api/employee/login,nginx接收到请求后转到http://localhost:8080/admin/,故最终的请求地址为http://localhost:8080/admin/employee/login,和后台服务的访问地址一致。

2)、负载均衡

    当如果服务以集群的方式进行部署时,那nginx在转发请求到服务器时就需要做相应的负载均衡。其实,负载均衡从本质上来说也是基于反向代理来实现的,最终都是转发请求。
#nginx 负载均衡的配置方式:
upstream webservers{  
    #**upstream:**如果代理服务器是一组服务器的话,我们可以使用upstream指令配置后端服务器组。
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
server{
    listen 80; #监听80端口号
    server_name localhost;
    
    location /api/{
        proxy_pass http://webservers/admin;#负载均衡
    }
}
#upstream后面的名称可自定义,但要上下保持一致。

**nginx 负载均衡策略:**
名称说明
轮询默认方式
weight权重方式,默认为1,权重越高,被分配的客户端请求就越多
ip_hash依据ip分配方式,这样每个访客可以固定访问一个后端服务
least_conn依据最少连接方式,把请求优先分配给连接数少的后端服务
url_hash依据url分配方式,这样相同的url会被分配到同一个后端服务
fair依据响应时间方式,响应时间短的服务将会被优先分配
**轮询【默认】【常用】:**
upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

weight:权重越高,被分配的客户端请求就越多
upstream webservers{
    server 192.168.100.128:8080 weight=90;
    server 192.168.100.129:8080 weight=10;
}

**ip_hash:依据ip分配方式,这样每个访客可以固定访问一个后端服务**
upstream webservers{
    ip_hash;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

**least_conn:**  依据最少连接方式,把请求优先分配给连接数少的后端服务
upstream webservers{
    least_conn;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

**url_hash:**依据url分配方式,这样相同的url会被分配到同一个后端服务
#url = http://localhost:8088/admin/employee?id=10
#uri = admin/employee?id=10
upstream webservers{
    hash $request_uri;
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}
**fair:**依据响应时间方式,响应时间短的服务将会被优先分配
upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
    fair;
}

3). 总结
Nginx的主要作用:

1. 部署静态资源
2. 反向代理
3. 负载均衡

6、MD5加密登录

修改数据库中明文密码,改为MD5加密后的密文:
String pwd = DigestUtils.md5DigestAsHex("123456".getBytes());
/#/  e10adc3949ba59abbe56e057f20f883e

7、导入接口文档

KaTeX parse error: Expected 'EOF', got '&' at position 86: …ses} \right\}连调&̲ 提测

第一步:定义接口,确定接口的路径、请求方式、传入参数、返回参数。

第二步:前端开发人员和后端开发人员并行开发,同时,也可自测。

第三步:前后端人员进行连调测试。

第四步:提交给测试人员进行最终测试。
    
操作步骤:
    1). 从资料中找到项目接口文件
    2). 导入到YApi平台 在YApi平台创建出两个项目,选择苍穹外卖-管理端接口.json导入

8、Swagger

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务(https://swagger.io/)。 它的主要作用是:

1. 使得前后端分离开发更加方便,有利于团队协作

2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担

3. 功能测试 

   Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。
                                                      
 knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望它能像一把匕首一样小巧,轻量,并且功能强悍!
                                                      
       目前,一般都使用knife4j框架。 

使用步骤

1、导入 knife4j 的maven坐标
在pom.xml中添加依赖
<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>
2、在配置类中加入 knife4j 相关配置

WebMvcConfiguration.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
/**
     * 设置静态资源映射
     * @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html")
            //knife4j-spring-ui-3.0.2.jar包里边
            .addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/");
}

常用的注解:

注解说明
@Api用在类上,例如Controller,表示对类的说明
@ApiModel用在类上,例如entity、DTO、VO
@ApiModelProperty用在属性上,描述属性信息
@ApiOperation用在方法上,例如Controller的方法,说明方法的用途、作用
@Api(tags = "员工相关接口")

    @PostMapping("/logout")
    @ApiOperation("员工退出")

@ApiModelProperty("用户名")
public class EmployeeLoginDTO implements Serializable {
    
}
@ApiModel(description = "员工登录返回的数据格式")
    public class EmployeeLoginVO implements Serializable {
    @ApiModelProperty("主键值")
    private Long id;

    @ApiModelProperty("用户名")
    private String userName;

    @ApiModelProperty("姓名")
    private String name;

    @ApiModelProperty("jwt令牌")
    private String token;
}

二、苍穹外卖2

1、ThreadLocal

ThreadLocal 并不是一个Thread,而是Thread的局部变量。 
    ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
    
**常用方法:**
- public void set(T value) 	设置当前线程的线程局部变量的值
- public T get() 		返回当前线程所对应的线程局部变量的值
- public void remove()        移除当前线程的线程局部变量

通过代码验证:当tomcat接受到请求后,拦截器 -> Controller -> Service -> Mapper(是不是同一个线程)


//通过线程ID来验证
System.out.println("当前线程ID:" + Thread.currentThread().getId());

实现记录操作人
初始工程在sky-common模块中已经封装了 ThreadLocal 操作的工具类:

package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();

    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}
●  在拦截器中解析出当前登录员工id,并放入线程局部变量中:
package com.sky.interceptor;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, 
                             HttpServletResponse response, 
                             Object handler) throws Exception {
        
		//.............................
       
        //2、校验令牌
        try {
            //.................
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:{}", empId);
            /将用户id存储到ThreadLocal
            BaseContext.setCurrentId(empId);
            
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //......................
        }
    }
    
    @Override
    //请求结束之前执行,清理ThreadLocal存储的ID
    public void afterCompletion(HttpServletRequest request, 
                                HttpServletResponse response, 
                                Object handler, Exception ex) throws Exception {
        BaseContext.removeCurrentId();
    }
}

2、pageQuery分页插件

   mybatis 的分页插件 PageHelper 来简化分页代码的开发。
   底层基于 mybatis 的拦截器实现。

在pom.xml文中添加依赖
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
</dependency>

/**
     * 分页查询
     *
     * @param employeePageQueryDTO
     * @return
     */
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        // select * from employee limit 0,10
        //开始分页查询
        PageHelper.startPage(employeePageQueryDTO.getPage(),
                             employeePageQueryDTO.getPageSize());

        Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);//后续定义

        long total = page.getTotal();
        List<Employee> records = page.getResult();

        return new PageResult(total, records);
}

//在 src/main/resources/mapper/EmployeeMapper.xml 中编写SQL:
 1、xml放置位置可以在配置文件中自行指定
mybatis:
   mapper-locations: classpath:mapper/*.xml #xml文件位置
   type-aliases-package: com.sky.entity #实体类所在包
2、方法返回类型为Page<Employee>,使用resultType指定时写Page中泛型的类型Employee即可
3com.sky.entity.Employee:包名+类名比较繁琐,可以进行简化:在配置中指定包名
    
<!--完整包名+类名-->
<!-- <select id="pageQuery" resultType="com.sky.entity.Employee"> -->
<!--在配置文件中指定包名后,只写类名即可:Employee, employee都可以-->   
<select id="pageQuery" resultType="Employee">
    select * from employee
    <where>
        <if test="name != null and name != ''">
            and name like concat('%',#{name},'%')
        </if>
    </where>
    order by create_time desc
</select>

3、完善时间显示

方式一:
    在Employee类的属性上加上注解,对日期进行格式化。
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; 
但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。
方式二(推荐):
    在WebMvcConfiguration中扩展SpringMVC的消息转换器,使用自定义的JacksonObjectMapper统一对日期类型进行格式处理:
    其中JacksonObjectMapper是通用的类已经定义好,直接使用即可。

package com.sky.json;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    //省略后续代码
}
/*******************************************************************************************************************************/
	/**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象,注意不要导错包是Jackson2Http
        MappingJackson2HttpMessageConverter converter = 
            new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        converters.add(0, converter);
    }

4、AOP切面编程

我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
AOP(面向切面编程):
重要名词:通知Advice(方法中的共性功能),切入点Pointcut(哪些方法), 切面Aspect(描述切入点和通知位置关系),通知类型(前置,后置:方法前边加还是后边加)
在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
★ 技术点:枚举、注解、AOP、反射.
1、自定义注解:
/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Documented //注解是否将包含在JavaDoc中
@Target(ElementType.METHOD) //指定注解可以加在什么地方(类,方法,成员变量)
@Retention(RetentionPolicy.RUNTIME) //定义注解的生命周期
public @interface AutoFill {
    
    //数据库操作类型:UPDATE INSERT
    OperationType value() default OperationType.INSERT;
    //使用value时候,@AutoFill(value = OperationType.UPDATE)
}

其中RetentionPolicy的不同策略对应的生命周期如下:
    
- RetentionPolicy.SOURCE : 仅存在于源代码中,编译阶段会被丢弃,不会包含于class字节码文件中。@Override, @SuppressWarnings都属于这类注解。
    
- RetentionPolicy.CLASS : 默认策略,在class字节码文件中存在,在类加载时被丢弃,运行时无法获取到
    
- RetentionPolicy.RUNTIME : 始终不会丢弃,可以使用反射获得该注解的信息。自定义的注解最常用的使用方式。
2、枚举定义:
    
package com.sky.enumeration;

/**
 * 数据库操作类型
 */
public enum OperationType {

    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT
}
3、切入点表达式:要进行增强的方法的描述方式 
//1.execution([访问修饰符]  返回值  包名.类/接口名.方法名(参数) [异常名])
execution(public User com.itheima.service.UserService.findById(int))
//2.当方法上使用指定注解时    
@annotation(com.sky.annotation.AutoFill)
4、自定义切面类
    
/**
 * 前置通知,在通知中进行公共字段的赋值
 */
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
    log.info("开始进行公共字段自动填充...");

    //获取到当前被拦截的方法上的数据库操作类型
    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) {
        //为4个公共字段赋值
        try {
            //通过方法名和参数类型获取定义好的方法:setCreateTime
            Method setCreateTime = entity.getClass().getDeclaredMethod(
                    AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
            Method setCreateUser = entity.getClass().getDeclaredMethod(
                    AutoFillConstant.SET_CREATE_USER, Long.class);
            Method setUpdateTime = entity.getClass().getDeclaredMethod(
                    AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod(
                    AutoFillConstant.SET_UPDATE_USER, Long.class);

            //通过反射为对象属性赋值
            setCreateTime.invoke(entity, now);
            setCreateUser.invoke(entity, currentId);
            setUpdateTime.invoke(entity, now);
            setUpdateUser.invoke(entity, currentId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    } else if (operationType == OperationType.UPDATE) {
        //为2个公共字段赋值
        try {
            Method setUpdateTime = entity.getClass().getDeclaredMethod(
                    AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod(
                    AutoFillConstant.SET_UPDATE_USER, Long.class);

            //通过反射为对象属性赋值
            setUpdateTime.invoke(entity, now);
            setUpdateUser.invoke(entity, currentId);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
} 
5、加入AutoFill注解
**Mapper接口的方法上加入 AutoFill 注解**

@Mapper
public interface CategoryMapper {
     /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Category category);
     /**
     * 根据id修改分类
     * @param category
     */
    @AutoFill(value = OperationType.UPDATE)
    void update(Category category);
}
**同时**,将业务层为公共字段赋值的代码注释掉。
1). 将员工管理的**新增****编辑**方法中的公共字段赋值的代码注释。
//employee.setCreateTime(LocalDateTime.now());
//employee.setUpdateTime(LocalDateTime.now());
//employee.setCreateUser(BaseContext.getCurrentId());
//employee.setUpdateUser(BaseContext.getCurrentId());
    
2). 将菜品分类管理的新增和修改方法中的公共字段赋值的代码注释。
//设置创建时间、修改时间、创建人、修改人
//category.setCreateTime(LocalDateTime.now());
//category.setUpdateTime(LocalDateTime.now());
//category.setCreateUser(BaseContext.getCurrentId());
//category.setUpdateUser(BaseContext.getCurrentId());

5、阿里云OSS存储

1). 定义OSS相关配置
# application-dev.yml
#sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
    access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
    #改成自己创建桶的名称
    bucket-name: sky-take-out
#application.yml
spring:
  profiles:
    active: dev    #设置环境
sky:
  alioss:
    endpoint: ${sky.alioss.endpoint}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket-name: ${sky.alioss.bucket-name}
2). 读取OSS配置【已定义】
    
在sky-common模块中
    
package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}
3).生成OSS工具类对象
    在sky-server
/**
 * 配置类,用于创建AliOssUtil对象
 */
@Configuration
@Slf4j
public class OssConfiguration {

    @Bean
    @ConditionalOnMissingBean //检查IoC容器,如果没有此对象再创建
    public AliOssUtil aliOssUtil(@Autowired AliOssProperties aliOssProperties){
        log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
        return new AliOssUtil(aliOssProperties.getEndpoint(),
                aliOssProperties.getAccessKeyId(),
                aliOssProperties.getAccessKeySecret(),
                aliOssProperties.getBucketName());
    }
}
AliOssUtil.java在sky-common模块中定义即可
这个是阿里云官方提供的API,只需完成响应的配置即可。
    
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);

        log.info("文件上传到:{}", stringBuilder.toString());

        return stringBuilder.toString();
    }
}
4).定义文件上传接口
    
/**
 * 通用接口
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

    @Autowired
    private AliOssUtil aliOssUtil;

    /**
     * 文件上传
     * @param file
     * @return
     */
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    public Result<String> upload(MultipartFile file){
        log.info("文件上传:{}",file);

        try {
            //原始文件名
            String originalFilename = file.getOriginalFilename();
            //截取原始文件名的后缀:abc.dfdfdf.png
            String extension = 
                originalFilename.substring(originalFilename.lastIndexOf("."));
            //构造新文件名称
            String objectName = UUID.randomUUID().toString() + extension;

            //文件的请求路径
            String filePath = aliOssUtil.upload(file.getBytes(), objectName);
            return Result.success(filePath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e.getMessage());
        }

        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

6、事务处理

按照产品原型中的要求,删除实现比较复杂:
- 起售中的菜品(status=1)不能删除
- 被套餐关联的菜品(setmeal_dish表)不能删除
- 删除菜品后,关联的口味数据(dish_flavor表)也需要删除掉

//1、检查菜品的状态,如果启售不能删除

//2、检查菜品是否被套餐引用,如果引用不能删除

//3、检查通过后,删除菜品基本信息和口味信息
@Transactional//事务
public void deleteBatch(List<Long> ids) {
        //判断当前菜品是否能够删除---是否存在起售中的菜品??
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);//后绪步骤实现
            if (dish.getStatus().equals(StatusConstant.ENABLE)) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }
        
        //上述检查方式可以进行如下优化
        //另外一种判断方式:SELECT id FROM dish WHERE id IN (51, 52, 53) AND STATUS = 1
         //select * from dish where id = 51
        //select * from dish where id = 52
         //select * from dish where id = 53

        //for循环外边
        //List<Long> = SELECT id FROM dish WHERE id IN (51, 52, 53) and status = 1
        //可自行实现

        //判断当前菜品是否能够删除---是否被套餐关联了??
        //select setmeal_id from setmeal_dish where dish_id IN (52,54,56)
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setmealIds != null && setmealIds.size() > 0) {
            //当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }

        //删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);//后绪步骤实现
            //删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);//后绪步骤实现
        }
    }

三、苍穹外卖3

1、Redis

 Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件。
     
**官网:**https://redis.io
**中文网:**https://www.redis.net.cn/
**key-value结构存储:**

**主要特点:**

- 基于内存存储,读写性能高  
- 适合存储**热点数据**(热点商品、资讯、新闻)
- 企业应用广泛
   Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供的数据是可以达到100000+QPS(每秒内查询10W次)。它存储的value类型比较丰富,也被称为结构化的NoSql数据库。
    NoSqlNot Only SQL),不仅仅是SQL,泛指**非关系型数据库**NoSql数据库并不是要取代关系型数据库,而是关系型数据库的补充。
    
**关系型数据库(RDBMS)**
- Mysql
- Oracle
- DB2
- SQLServer
    
**非关系型数据库(NoSql)**
- Redis
- Mongo db
- MemCached
Redis安装包分为windows版和Linux版:

- Windows版下载地址:https://github.com/microsoftarchive/redis/releases
- Linux版下载地址: https://download.redis.io/releases/ 

Redis的安装
1)在Windows中安装Redis(项目中使用)    
RedisWindows版属于**绿色软件**,直接解压即可使用。
redis-cli.exe: Redis客户端redis-client
        
2)在Linux中安装Redis(简单了解)
        
> - 后续学完Linux课程再操作
> - mac系统安装和Linux一样
> - mac不想自己编译安装,参考:https://blog.csdn.net/realize_dream/article/details/106227622Linux系统安装Redis步骤:

1.Redis安装包上传到Linux
2. 解压安装包,命令:tar -zxvf redis-4.0.0.tar.gz -C /usr/local
3. 安装Redis的依赖环境gcc,命令:yum install gcc-c++
4. 进入/usr/local/redis-4.0.0,进行编译,命令:make
5. 进入redis的src目录进行安装,命令:make install

安装后重点文件说明:

- /usr/local/redis-4.0.0/src/redis-server:Redis服务启动脚本
- /usr/local/redis-4.0.0/src/redis-cli:Redis客户端脚本
- /usr/local/redis-4.0.0/redis.conf:Redis配置文件

2、Redis服务启动与停止

服务启动命令
   #手动指定配置文件
redis-server.exe redis.windows.conf
 
Redis服务默认端口号为 **6379** ,通过快捷键**Ctrl + C** 即可停止Redis服务

当Redis服务启动成功后,可通过客户端进行连接。
 
客户端连接命令 : redis-cli.exe
通过redis-cli.exe命令默认连接的是本地的redis服务,并且使用默认6379端口。也可以通过指定如下参数连接:
redis-cli.exe -h 127.0.0.1 -p 6379 -a 123456
- -h ip地址
- -p 端口号
- -a 密码(如果需要)
设置Redis服务密码,修改redis.windows.conf
#requirepass password
requirepass 123456

**注意:**
- **修改密码后需要重启Redis服务才能生效**
- Redis配置文件中 # 表示注释

重启Redis后,再次连接Redis时,需加上密码,否则**能连接成功但无法执行指令**
redis-cli.exe -h localhost -p 6379 -a 123456
#a: auth校验
此时,-h 和 -p 参数可省略不写。

3、Redis客户端图形化工具及数据类型

默认提供的客户端连接工具界面不太友好,同时操作也较为麻烦,接下来,引入一个Redis客户端图形工具。

安装完毕后,直接双击启动
> mac或者linux下载地址:https://goanother.com/cn/#download
Redis存储的是key-value结构的数据,其中**key一般使用字符串**,value有**5种常用**的数据类型:

- 字符串 string
- 哈希 hash
- 列表 list
- 集合 set
- 有序集合 sorted set / zset
- bitmap 位图类型
- geo 地理位置类型
- HyperLogLog 基数统计类型

- 字符串(string):普通字符串,Redis中最简单的数据类型
- 哈希(hash):也叫散列,类似于Java中的HashMap结构
- 列表(list):按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList
- 集合(set):无序集合,没有重复元素,类似于Java中的HashSet
- 有序集合(sorted set/zset):集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
new HashMap<String, 常用的5种类型>();

1new HashMap<String, String>();
2new HashMap<String, new HashMap<>>();
3new HashMap<String, new ArrayList<>>();
4new HashMap<String, new HashSet<>>();
5new HashMap<String, new 有序set<>>();

4、Redis常用命令

Redis 中字符串类型常用命令:

- SET key value 					 设置指定key的值
- GET key                             获取指定key的值
- SETE key seconds value         设置指定key的值,并将 key 的过期时间设为 seconds 秒

set ex(expire:过期)

- SETNX key value                 只有在 key不存在时设置 key 的值

set nx(not exist:不存在)

更多命令可以参考Redis中文网:https://www.redis.net.cn
哈希操作:

Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:

- HSET key field value       将哈希表 key 中的字段 field 的值设为 value

  hset: hash set

- HGET key field             获取存储在哈希表中指定字段的值

  hget: hast get

- HDEL key field             删除存储在哈希表中的指定字段

- HKEYS key                  获取哈希表中所有字段

- HVALS key                  获取哈希表中所有值

  hvals: hash values

- HGETALL key                获取在哈希表中指定 key 的所有字段和值

  hgetall: hast get all
  
企业开发中常用格式:

hset students 101 "{name:zhangsan, age:20}"
hset students 102 "{name:lisi, age:25}"
列表list操作

Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:

LPUSH  key value1 [value2]         将一个或多个值插入到列表头部

lpush mylist d c b a
#list push中可以保存相同的元素
#lpush mylist a b
#right push: rpush

LRANGE key start stop                获取列表指定范围内的元素
lrange mylist 0 -1
#-1代表list中最后一个元素的位置

- RPOP key                    移除并获取列表最后一个元素

- LLEN key                     获取列表长度

- BRPOP key1 [key2 ] timeout   移出并获取列表的最后一个元素, 如果列表没有元素会阻塞直到等待超时或发现可弹出元素为止

brpop: block right pop
集合set操作
Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不会出现重复的数据,常用命令:

- SADD key member1 [member2]            向集合添加一个或多个成员
sadd myset a b c d a b

- SMEMBERS key                         返回集合中的所有成员
SMEMBERS myset
#返回结果:a b c d 
#发现没有重复的元素

- SCARD key                         获取集合的成员数

- SINTER key1 [key2]             返回给定所有集合的交集
sadd myset2 c d e f

#求myset和myset2的交集:两个集合中都有的元素
sinter myset myset2
#会输出c d 

- SUNION key1 [key2]                 返回所有给定集合的并集
sunion myset myset2
#会输出:a b c d e f

- SREM key member1 [member2]        移除集合中一个或多个成员
#srem: set remove
srem myset a b 
有序集合zset操作
 
   Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数(score)。
   redis正是通过分数来为集合中的成员进行从小到大排序。有序集合的成员是唯一的,但分数却可以重复。
   
常用命令:

- ZADD key score1 member1 [score2 member2]     向有序集合添加一个或多个成员,或者更新已存在成员的 分数
zadd myzset 80 wangwu
zadd myzset 100 zhangsan 60 lisi

- ZRANGE key start stop [WITHSCORES]    通过索引区间返回有序集合中指定区间内的成员
#默认是升序:从小到大(根据score)
zrange myzset 0 -1
#1) "lisi"
#2) "wangwu"
#3) "zhangsan"

#如果想倒序:zrevrange key start stop [WITHSCORES]     
#revert:反转
zrevrange myzset 0 -1
#1) "zhangsan"
#2) "wangwu"
#3) "lisi"

- ZINCRBY key increment member   有序集合中对指定成员的分数加上增量 increment
#给wangwu增加30分
#increment:增长
zincrby myzset 30 wangwu
zrevrange myzset 0 -1

- ZREM key member [member ...]     移除有序集合中的一个或多个成员
通用命令
Redis的通用命令是不分数据类型的,都可以使用的命令:

- KEYS pattern 	    查找所有符合给定模式( pattern)的 key 

- EXISTS key 		检查给定 key 是否存在

- TYPE key 		    返回 key 所储存的值的类型

- DEL key 		    该命令用于在 key 存在是删除 key

- TTL key           返回给定 key 的剩余生存时间(TTL, time to live),以秒为单位
共有3种返回结果:
-1: 没有过期时间,永久有效
-2:已经过期或者不存在这个一个key
正数:代表还能在内存中存活的时间,单位:秒

5、Spring Data Redis使用方式

Redis 的 Java 客户端很多,常用的几种:

- Jedis: 在java中操作Redis

- Lettuce

- Spring Data Redis

Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即 spring-boot-starter-data-redis。

重点学习Spring Data Redis。
1).导入Spring Boot提供了对应的Starter,maven坐标:
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

- ValueOperations:string数据操作

- SetOperations:set类型数据操作

- ZSetOperations:zset类型数据操作

- HashOperations:hash类型的数据操作

- ListOperations:list类型的数据操作
2). 配置Redis数据源
#sky:
  redis:
    host: localhost
    port: 6379
    #设置密码,没有设置则注释掉
    password: 123456
    database: 0
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15 
#可以使用select 切换不同的数据库
select 0
select 1
在application.yml中添加读取application-dev.yml中的相关Redis配置

spring:
  profiles:
    active: dev
  redis:
    host: ${sky.redis.host}
    port: ${sky.redis.port}
    password: ${sky.redis.password}
    database: ${sky.redis.database}
3). 编写配置类,创建RedisTemplate对象


@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

/*********************************************************************************/
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象:RedisAutoConfiguration,但是默认的key序列化器为

JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为StringRedisSerializer序列化器。
    
//jar包中RedisAutoConfiguration类的源码
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
   RedisTemplate<Object, Object> template = new RedisTemplate<>();
   template.setConnectionFactory(redisConnectionFactory);
   return template;
}
4). 通过RedisTemplate对象操作Redis

@SpringBootTest
public class SpringDataRedisTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test //注意:使用长的import org.junit.jupiter.api.Test;
    public void testRedisTemplate(){
        System.out.println(redisTemplate);
        //string数据操作
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //hash类型的数据操作
        HashOperations hashOperations = redisTemplate.opsForHash();
        //list类型的数据操作
        ListOperations listOperations = redisTemplate.opsForList();
        //set类型数据操作
        SetOperations setOperations = redisTemplate.opsForSet();
        //zset类型数据操作
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }
}

6、测试实例

1). 操作字符串类型数据
     //set name 小明
        redisTemplate.opsForValue().set("name","小明");
        //get name
        String city = (String) redisTemplate.opsForValue().get("name");
        System.out.println(city);
2). 操作哈希类型数据
     HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.put("100","name","tom");
        hashOperations.put("100","age","20");
        String name = (String) hashOperations.get("100", "name");
        System.out.println(name);
3). 操作列表类型数据
    ListOperations listOperations = redisTemplate.opsForList();
        listOperations.leftPushAll("mylist","a","b","c");
        listOperations.leftPush("mylist","d");
        List mylist = listOperations.range("mylist", 0, -1);
        System.out.println(mylist);
4). 操作集合类型数据
       SetOperations setOperations = redisTemplate.opsForSet();
        setOperations.add("set1","a","b","c","d");
        setOperations.add("set2","a","b","x","y");
        Set members = setOperations.members("set1");
        System.out.println(members);
        Long size = setOperations.size("set1");
        System.out.println(size);
5). 操作有序集合类型数据
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
        zSetOperations.add("zset1","a",10);
        zSetOperations.add("zset1","c",9);
        //默认升序
        Set zset1 = zSetOperations.range("zset1", 0, -1);
        System.out.println(zset1);
        //降序:进行反转reverse
        zset1 = zSetOperations.reverseRange("zset1", 0, -1);
        System.out.println(zset1);
6). 通用命令操作
     Set keys = redisTemplate.keys("*");
    System.out.println(keys);
    //是否存在:have
    Boolean name = redisTemplate.hasKey("name");
    System.out.println("是否存在name: " + name);
    Boolean set1 = redisTemplate.hasKey("set1");
    System.out.println("是否存在set1: " + set1);
    for (Object key : keys) {
        DataType type = redisTemplate.type(key);
        System.out.println(type.name());
    }
    redisTemplate.delete("mylist");
    //查询剩余存活时间:time to live
    //expire: 过期
    Long ttl = redisTemplate.getExpire("set1");
    System.out.println(ttl);

7、数据同步

   为了保证数据库和Redis中的数据保持一致,修改管理端接口 DishController的相关方法,加入清理缓存逻辑。
   
- 注意:在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致
- 如果MySQL中的数据发生变化,需要及时清理缓存数据。否则就会造成缓存数据与数据库数据不一致的情况。
抽取清理缓存的方法:
在管理端DishController中添加
新增菜品优化
   @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavor(dishDTO);

        //==========================新增代码=====================
        //清理缓存数据
        String key = "dish_" + dishDTO.getCategoryId();
        cleanCache(key);
        return Result.success();
    }
菜品批量删除优化
//==========================新增代码=====================
        //将所有的菜品缓存数据清理掉,所有以dish_开头的key
        cleanCache("dish_*");
修改菜品优化
 //==========================新增代码=====================
        cleanCache("dish_" + dishDTO.getCategoryId());
菜品起售停售优化
//==========================新增代码=====================
        //将所有的菜品缓存数据清理掉,所有以dish_开头的key
        cleanCache("dish_*");

8、Spring Cache 框架

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。

- Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:
- EHCache
- Caffeine
- Redis(常用)
起步依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-cache</artifactId>  		            		       	 <version>2.7.3</version> 
</dependency>

常用的注解
注解说明
@EnableCaching开启缓存注解功能,通常加在启动类上
@Cacheable在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut将方法的返回值放到缓存中
@CacheEvict将一条或多条数据从缓存中删除
    在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
    使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
    
创建名为spring_cache_demo数据库,将springcachedemo.sql脚本直接导入数据库中。
引导类上加@EnableCaching:

@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
        log.info("项目启动成功...");
    }
}
检查密码:application.yml

#数据库密码和Redis密码
spring:
  datasource:
    druid:
      username: root
      password: root
  redis:
    host: localhost
    port: 6379
    password: 123456
2). @CachePut注解

@CachePut 说明:

​	作用: 将方法返回值,放入缓存

​	value: 缓存的名称, 每个缓存名称下面可以有很多key

​	key: 缓存的key  ----------> 支持Spring的表达式语言SPEL语法

在save方法上加注解@CachePut
当前UserController的save方法是用来保存用户信息的,在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:
   /*
	 CachePut:将方法返回值放入缓存
	 value:缓存的名称,每个缓存名称下面可以有多个key
	 key:缓存的key
	*/
	@PostMapping
    //@CachePut(value = "userCache", key = "#resutl.id")
    @CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

#user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;
#result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;
    
了解:
    
#p0.id:#p0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key 
p: param

#a0.id:#a0指的是方法中的第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key 
a: argument

#root.args[0].id:#root.args[0]指的是方法中第一个参数,id指的是第一个参数的id属性,也就是使用第一个参数的id属性作为key 
3). @Cacheable注解
    
作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中

​	value: 缓存的名称,每个缓存名称下面可以有多个key

​	key: 缓存的key  ----------> 支持Spring的表达式语言SPEL语法
    
在getById上加注解@Cacheable

	/**
	* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,	  *调用方法并将方法返回值放到缓存中
	* value:缓存的名称,每个缓存名称下面可以有多个key
	* key:缓存的key
	*/
	@GetMapping
    @Cacheable(cacheNames = "userCache",key="#id")
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }
4). @CacheEvict注解

@CacheEvict 说明:
    作用: 清理指定缓存

​	value: 缓存的名称,每个缓存名称下面可以有多个key

​	key: 缓存的key  ----------> 支持Spring的表达式语言SPEL语法
在 delete 方法上加注解@CacheEvict
    
    
	@DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

	@DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
    public void deleteAll(){
        userMapper.deleteAll();
    }
5). 缓存非null值【扩展】
    思考:如果getById时查询的是一个不存在的用户,那么会缓存一个null值,此时就毫无意义,该如何判断下如果是null就不缓存呢?
    
condition : 表示满足条件, 再进行缓存 ;
unless : 表示满足条件则不缓存 ; 与上述的condition是反向的 ;

/**
 * Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中
 * value:缓存的名称,每个缓存名称下面可以有多个key
 * key:缓存的key
 * condition:条件,满足条件时才缓存数据
 * unless:满足条件则不缓存
 */
@Cacheable(value = "userCache",key = "#id", unless = "#result == null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
    User user = userService.getById(id);
    return user;
}

#注意
#此处,我们只能使用 unless(什么时候不存储)
#1.condition中只能使用方法参数中数据:如果使用#result,不管是否查询到数据都是返回null
#2.unless才能使用方法返回值中的数据

四、苍穹外卖(小程序端)

1.HttpClient

客户端
发送Http请求
服务端
接受响应数据
    HttpClient 是Apache Jakarta Common 下的子项目,可以用来提供高效的、最新的、功能丰富的支持 HTTP 协议的客户端编程工具包,并且它支持 HTTP 协议最新的版本和建议。
    
    优点:支持连接池、多线程
    
HttpClient作用:
   
- 发送HTTP请求
- 接收响应数据

HttpClient应用场景:
    当我们在使用扫描支付、查看地图、获取验证码、查看天气等功能时。
    应用程序本身并未实现这些功能,都是在应用程序里访问提供这些功能的服务,访问这些服务需要发送HTTP请求,并且接收响应数据,可通过HttpClient来实现。‘
   
HttpClient的maven坐标:

<dependency>
	<groupId>org.apache.httpcomponents</groupId>
	<artifactId>httpclient</artifactId>
	<version>4.5.13</version>
</dependency>

2、HttpClient的核心API

- HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。
- HttpClients:可认为是构建器,可创建HttpClient对象。
- CloseableHttpClient:实现类,实现了HttpClient接口。
- HttpGet:Get方式请求类型。
- HttpPost:Post方式请求类型。
- put
- delete

HttpClient发送请求步骤:

- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求

其他:
    OkHttp、RestTemplate(SpringMVC)……
    
应该导入HttpClient相关的坐标
<dependency>
    <groupId>com.aliyun.oss</groupId>
    <artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!--阿里云上传:底层是用HttpClient将上传的请求发送到阿里云服务器-->
1GET方式请求
    
@SpringBootTest
public class HttpClientTest {

    /**
     * 测试通过httpclient发送GET方式的请求
     */
    @Test
    public void testGET() throws Exception{
        //创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        //创建请求对象
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

        //发送请求,接受响应结果
        CloseableHttpResponse response = httpClient.execute(httpGet);

        //获取服务端返回的状态码
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("服务端返回的状态码为:" + statusCode);

        HttpEntity entity = response.getEntity();
        String body = EntityUtils.toString(entity);
        System.out.println("服务端返回的数据为:" + body);

        //关闭资源
        response.close();
        httpClient.close();
    }
}
 2.POST方式请求
	/**
     * 测试通过httpclient发送POST方式的请求
     */
@Test
public void testPOST() throws Exception{
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        //创建请求对象
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");

        //jsonObject.toString() => {username: admin, password:123456}
        StringEntity entity = new StringEntity(jsonObject.toString());
        //指定请求编码方式
        entity.setContentEncoding("utf-8");
        //数据格式
        entity.setContentType("application/json");
        httpPost.setEntity(entity);

        //发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);

        //解析返回结果
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("响应码为:" + statusCode);

        HttpEntity entity1 = response.getEntity();
        String body = EntityUtils.toString(entity1);
        System.out.println("响应数据为:" + body);

        //关闭资源
        response.close();
        httpClient.close();
}

3、微信小程序开发

- 注册小程序

- 完善小程序信息
  -不能选择小游戏类目
  
- 下载开发者工具 
 
1). 注册小程序
注册地址:https://mp.weixin.qq.com/wxopen/waregister?action=step1

2). 完善小程序信息
    登录小程序后台:https://mp.weixin.qq.com/
    完善小程序信息、小程序类目,不能选择小游戏类目,查看小程序的 AppIDAppSecret
        
3). 下载开发者工具

    其他系统下载地址: https://developers.weixin.qq.com/miniprogram/dev/devtools/stable.html 

熟悉开发者工具布局
设置不校验合法域名
**注:**开发阶段,小程序发出请求到后端的Tomcat服务器,若不勾选,请求发送失败。
- 小程序目录结构

    小程序包含一个描述整体程序的 app 和多个描述各自页面的 page。主体部分由三个文件组成,必须放在项目的根目录。
    
文件说明:

- app.js: 必须存在,主要存放小程序的逻辑代码

- app.json:必须存在,小程序配置文件,主要存放小程序的公共配置

- app.wxss: 非必须存在,主要存放小程序公共样式表,类似于前端的CSS样式

- js文件:必须存在,存放页面业务逻辑代码,编写的js代码。

- json文件:非必须,存放页面相关的配置。

- wxml文件:必须存在,存放页面结构,主要是做页面布局,页面效果展示的,类似于HTML页面。

- wxss文件:非必须,存放页面样式表,相当于CSS文件。

4、编写和编译小程序

1). 编写
进入到index.wxml,编写页面布局

<view class="container">
  <view>{{msg}}</view>
   <view>
    <button type="default" bindtap="getUserInfo">获取用户信息</button>
    <image style="width: 100px;height: 100px;" src="{{avatarUrl}}"></image>
    {{nickName}}
  </view>
   <view>
    <button type="primary" bindtap="wxlogin">微信登录</button>
    授权码:{{code}}
  </view>
   <view>
    <button type="warn" bindtap="sendRequest">发送请求</button>
    响应结果:{{result}}
  </view>
</view>
进入到index.js,编写业务逻辑代码

Page({
  data:{
    msg:'hello world',
    avatarUrl:'',
    nickName:'',
    code:'',
    result:''
  },
  getUserInfo:function(){
    wx.getUserProfile({
      desc: '获取用户信息',
      success:(res) => {
        console.log(res.userInfo)
        this.setData({
          avatarUrl:res.userInfo.avatarUrl,
          nickName:res.userInfo.nickName
        })
      }
    })
  },
  wxlogin:function(){
    wx.login({
      success: (res) => {
        console.log("授权码:"+res.code)
        this.setData({
          code:res.code
        })
      }
    })
  },
  sendRequest:function(){
    //通过微信小程序向项目服务器Tomcat发送一个请求
    wx.request({
      url: 'http://localhost:8080/user/shop/status',
      method:'GET',
      success:(res) => {
        console.log("响应结果:" + res.data.data)
        this.setData({
          result:res.data.data
        })
      }
    })
  }})
点击编译按钮进行运行
为了防止小程序开发者滥用用户昵称和头像,官方停用了接口;如果想看效果需要切换到旧版基础库。

5、发布小程序

小程序的代码都已经开发完毕,要将小程序发布上线,让所有的用户都能使用到这个小程序。
    点击上传按钮:
    指定版本号:
    上传成功:
当前小程序版本只是一个开发版本。
进到微信公众平台,打开版本管理页面。
需提交审核,变成审核版本,审核通过后,进行发布,变成线上版本。

6、导入小程序代码

开发微信小程序,本质上是属于前端的开发
1). 找到资料
2). 导入代码
AppID:使用自己的AppID

3). 查看项目结构
主体的文件:app.js app.json app.wxss
项目的页面比较多,主要存放在pages目录。

4). 修改配置
因为小程序要请求后端服务,需要修改为自己后端服务的ip地址和端口号(默认不需要修改)
common-->vendor.js-->搜索(ctrl+f)-->baseUri

- 微信登录流程
 微信登录:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
 
1. 小程序端,调用wx.login()获取code,就是授权码。
2. 小程序端,调用wx.request()发送请求并携带code,请求开发者服务器(自己编写的后端服务)。
3. 开发者服务端,通过HttpClient向微信接口服务发送请求,并携带appId+appsecret+code三个参数。
4. 开发者服务端,接收微信接口服务返回的数据,session_key+opendId等。opendId是微信用户的唯一标识。
5. 开发者服务端,自定义登录态,生成令牌(token)和openid等数据返回给小程序端,方便后绪请求身份校验。
6. 小程序端,收到自定义登录态,存储storage。
7. 小程序端,后绪通过wx.request()发起业务请求时,携带token。
8. 开发者服务端,收到请求后,通过携带的token,解析当前登录用户的id。
9. 开发者服务端,身份校验通过后,继续相关的业务逻辑处理,最终返回业务数据。

说明:
10. 调用https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html 获取 临时登录凭证code ,并回传到开发者服务器。
11. 调用https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html 接口,
换取 用户唯一标识 OpenID 、 用户在微信开放平台帐号下的唯一标识UnionID(若当前小程序已绑定到微信开放平台帐号) 和 会话密钥 session_key。

7、配置微信登录所需配置项

定义相关配置

配置微信登录所需配置项:
application-dev.yml
#sky: 改成自己的小程序id和secret
  wechat:
    appid: wxffb3637a228223b8
    secret: 84311df9199ecacdf4f12d27b6b9522d
application.yml
#sky:
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}
配置为微信用户生成jwt令牌时使用的配置项:
application.yml
#sky:
  #jwt:
    # 省略......
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication
用户端拦截器
    
    
/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {

    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        //==========================================
        //注意HandlerMethod导包:org.springframework.web.method.HandlerMethod
        if (!(handler instanceof org.springframework.web.method.HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前用户的id:{}", userId);
            BaseContext.setCurrentId(userId); //放到ThreadLocal里边
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
/***************************************************************************************************/WebMvcConfiguration配置类中注册拦截器
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
	/**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        //.........

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
}

8、订单支付

    要实现微信支付就需要注册微信支付的一个商户号,这个商户号是必须要有一家企业并且有正规的营业执照。只有具备了这些资质之后,才可以去注册商户号,才能开通支付权限。
    微信支付产品:本项目选择小程序支付
    参考:https://pay.weixin.qq.com/static/product/product_index.shtml
微信小程序支付时序图:

在这里插入图片描述

微信支付相关接口:
JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单(对应时序图的第5步)

- https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_1.shtml

微信小程序调起支付:通过JSAPI下单接口获取到发起支付的必要参数prepay_id,然后使用微信支付提供的小程序方法调起小程序支付(对应时序图的第10步)

- https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_4.shtml

完成微信支付有两个关键的步骤:

- 第一个 就是需要在商户系统当中调用微信后台的一个下单接口,就是生成预支付交易单。

- 第二个 就是支付成功之后微信后台会给推送消息。
解决:微信提供的方式就是对数据进行加密、解密、签名多种方式。要完成数据加密解密,需要提前准备相应的一些文件,其实就是一些证书。

获取微信支付平台证书、商户私钥文件.

目前,商户系统它的ip地址就是当前自己电脑的ip地址,只是一个局域网内的ip地址,微信后台无法调用到。
**内网穿透**

  通过cpolar软件可以获得一个临时域名,而这个临时域名是一个公网ip,这样,微信后台就可以请求到商户系统。
cpolar软件的使用:

1). 下载与安装

下载地址:https://dashboard.cpolar.com/get-started

2). cpolar指定authtoken
复制authtoken:https://dashboard.cpolar.com/auth

执行命令:
#Windows:
cpolar.exe authtoken MmJiMTBiZDAtMz333330ZWI5LTlhOTQtODE1ZjcxNmZhOGRl

3). 获取临时域名

执行命令:cpolar.exe http 8080

4). 验证临时域名有效性

启动项目,访问接口文档:http://localhost:8080/doc.html 使用临时域名访问,证明临时域名生效
**微信支付相关配置**

#sky:
  #wechat:
    #appid: wxffb3637a228223b8
    #secret: 84311df9199ecacdf4f12d27b6b9522d
    
    #支付需要用到的配置
    mchid : 1561414331 #支付商户号
    mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606 #商户编号
    apiV3Key: CZBK51236435wxpay435434323FFDuv3
    privateKeyFilePath: D:\apiclient_key.pem
    weChatPayCertFilePath: D:\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
    #设置微信支付回调接口
    notifyUrl: http://5c968a9a.r1.cpolar.top/notify/paySuccess
    #退款回调接口
    refundNotifyUrl: https://www.weixin.qq.com/wxpay/refund
    
    
application.yml
#sky:
  #wechat:
    #appid: ${sky.wechat.appid}
    #secret: ${sky.wechat.secret}
    
    #微信支付需要用到的配置
    mchid : ${sky.wechat.mchid}
    mchSerialNo: ${sky.wechat.mchSerialNo}
    privateKeyFilePath: ${sky.wechat.privateKeyFilePath}
    apiV3Key: ${sky.wechat.apiV3Key}
    weChatPayCertFilePath: ${sky.wechat.weChatPayCertFilePath}
    notifyUrl: ${sky.wechat.notifyUrl}
    refundNotifyUrl: ${sky.wechat.refundNotifyUrl}
WeChatProperties.java:读取配置

@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址
}
Service层
     
在OrderService.java中添加payment和paySuccess两个方法定义
	/**
     * 订单支付
     * @param ordersPaymentDTO
     * @return
     */
    OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;

    /**
     * 获取支付结果,修改订单状态
     * @param outTradeNo
     */
    void paySuccess(String outTradeNo);
/*************************************************************************/
PayNotifyController.java
    
/**
 * 支付回调相关接口
 */
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 支付成功回调
     *
     * @param request
     */
    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

    /**
     * 读取数据
     *
     * @param request
     * @return
     * @throws Exception
     */
    private String readData(HttpServletRequest request) throws Exception {
        BufferedReader reader = request.getReader();
        StringBuilder result = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            if (result.length() > 0) {
                result.append("\n");
            }
            result.append(line);
        }
        return result.toString();
    }

    /**
     * 数据解密
     *
     * @param body
     * @return
     * @throws Exception
     */
    private String decryptData(String body) throws Exception {
        JSONObject resultObject = JSON.parseObject(body);
        JSONObject resource = resultObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //密文解密
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        return plainText;
    }

    /**
     * 给微信响应
     * @param response
     */
    private void responseToWeixin(HttpServletResponse response) throws Exception{
        response.setStatus(200);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", "SUCCESS");
        map.put("message", "SUCCESS");
        response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
        response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    }
}

五、苍穹外卖

1、注册百度地图服务

1. 基于百度地图开放平台实现(https://lbsyun.baidu.com/)

2.注册账号--->控制台--->我的应用-->创建应用获取AK(服务端应用)--->调用接口
创建应用时:
类型:选服务端
#IP白名单:0.0.0.0/0 

3.相关接口
地理编码服务:https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding

  地理编码服务(又名Geocoder)是一类Web API接口服务;
地理编码服务提供将结构化地址数据(如:北京市海淀区上地十街十号)转换为对应坐标点(经纬度)功能;

GET: https://api.map.baidu.com/geocoding/v3/?address=北京市海淀区上地十街10号&output=json&ak=您的ak
在url中传递3个参数即可,返回数据格式如下:Java中将返回的json字符串转成JSONObject
{ // JSONObject
    "status": 0, // jsonObject.getIntValue("status")
    "result": { //对象: jsonObject.getJSONObject("result")
        "location": { //对象: jsonObject.getJSONObject("location") 
            "lng": 116.3076223267197, //经度 getString("lng")
            "lat": 40.05682848596073 //纬度 getString("lat")
        },
        "precise": 1,
        "confidence": 80,
        "comprehension": 100,
        "level": "门址"
    }
}
路线规划服务:https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1
轻量级路线规划服务(又名DirectionLite API )是一套REST风格的Web服务API,以HTTP/HTTPS形式提供了路线规划服务。相较于Direction API,DirectionLite API更注重服务的高性能和接口的轻便简洁,满足基础的路线规划需求,并不具备Direciton API中的驾车多路线/未来出行和公交跨城规划等高级功能。DirectionLite API支持驾车、骑行、步行、公交路线规划,支持中国大陆地区。

GET: https://api.map.baidu.com/directionlite/v1/driving?origin=40.01116,116.339303&destination=39.936404,116.452562&steps_info=0&ak=您的AK
传递4个参数即可,返回数据如下:
{
    "status": 0, //getIntValue
    "message": "ok",
    "result": { //对象: getJSONObject
        "origin": {
            "lng": 116.33929505188,
            "lat": 40.011157363344
        },
        "destination": {
            "lng": 116.45255341058,
            "lat": 39.936401378723
        },
        "routes": [ //数组: result.getJSONArray("routes");
            {
                "distance": 18129, //getString 得到的两地间的距离
                "duration": 6193
            }
        ]
    }
}

2、检验配送距离

商家门店地址可以配置在配置文件中,例如:application.yml
#sky:
  shop:
    address: 北京市海淀区上地十街10号
  baidu:
    ak: sdfsdfsdfsd #百度应用id
用户下单时添加校验代码:
//两项用途过少没有必要新建一个配置类 
@Value("${sky.shop.address}")//将yml文件中的属性赋值到shopAddress中
private String shopAddress;

@Value("${sky.baidu.ak}")
private String ak;

/**
 * 检查客户的收货地址是否超出配送范围
 * @param address 收货地址
 */
private void checkOutOfRange(String address) {

    //1、调用百度地理编码服务根据店铺地址获取经纬度
    String shopGeo = getGeoByAddress(shopAddress);

    //2、调用地理编码服务服务根据用户配送地址获取经纬度
    String userGeo = getGeoByAddress(address);

    //3、调用路线规划服务对两个地址进行规划,根据返回的距离判断是否在配送范围
    Integer distance = getDistance(shopGeo, userGeo);

    if(distance > 5000){
        //配送距离超过5000米
        throw new OrderBusinessException("超出配送范围: " + distance);
    }
}
编写两个私有方法:getGeoByAddress和getDistance,在方法中用HttpClientUtil.doGet()调用百度接口

/**
 * 根据详细地址获取经纬度坐标
 * 地理编码服务:https://lbsyun.baidu.com/index.php?title=webapi/gui
 *
 * @param address 详细地址(包含省市区)
 * @return 经纬度,格式为:纬度,经度;小数点后不超过6位,40.056878,116.30815
 */
private String getGeoByAddress(String address) {
    
    //1、使用map构建请求参数
    Map<String, String> map = new HashMap<>();
    map.put("address",address); //地址
    map.put("output","json");//指定返回数据格式
    map.put("ak",ak);
    
    //2、调用百度地图服务:获取经纬度坐标
    String json =
            HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);
    log.info("地址: {},返回值:{}", address, json);
    
    //3、解析响应结果
    JSONObject jsonObject = JSON.parseObject(json);
    //status=0,表示成功
    if(jsonObject.getIntValue("status") != 0){
        throw new OrderBusinessException("地址解析失败");
    }
    //数据解析
    JSONObject location =
            jsonObject.getJSONObject("result").getJSONObject(
    String lat = location.getString("lat"); //纬度
    String lng = location.getString("lng"); //经度
    //返回经纬度坐标
    return lat + "," + lng;
}
完善下单功能:

public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {
    //1、下单数据校验(收货地址为空、超出配送范围、购物车为空)
    Long userId = BaseContext.getCurrentId();
    //1.1 收货地址为空
    AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
    if (addressBook == null) {
        throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
    }

    //====================================================新增代码
    //1.2 超出配送范围
    checkOutOfRange(addressBook.getProvinceName() +
            addressBook.getCityName() + addressBook.getDetail());

    //1.3 购物车为空(userId)
    //……省略其他代码
}

3、定时任务Spring Task

Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

定位:定时任务框架
作用:定时自动执行某段Java代码

应用场景:

1). 信用卡每月还款提醒

2). 银行贷款每月还款提醒

3). 火车票售票系统处理未支付订单

4). 入职纪念日为用户发送通知

**强调:只要是需要定时处理的场景都可以使用Spring Task

**cron表达式**

- 其实就是一个字符串,通过cron表达式可以定义任务触发的时间

- 构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义:秒、分钟、小时、日、月、周、年(可选),每部分的含义如下表所示:
组成部分含义取值范围
第一部分Seconds (秒)0-59
第二部分Minutes(分)0-59
第三部分Hours(时)0-23
第四部分Day-of-Month(天)1-31
第五部分Month(月)0-11或JAN-DEC
第六部分Day-of-Week(星期)1-7(1表示星期日)或SUN-SAT
第七部分Year(年) 可选1970-2099
cron表达式还可以包含一些特殊符号来设置更加灵活的定时规则, 如下表所示:
符号含义
?不确定的值。当两个子表达式其中一个被指定了值以后,为了避免冲突,需要将另外一个的值为“?” 例如:想在每月20日触发调度,不管20号是星期几,只能用如下写法:0 0 0 20 * ? 其中最后一个只能用“?”
*代表所有可能的值
,设置多个值,例如”26,29,33”表示在26分,29分和33分各自运行一次任务
-设置取值范围,例如”5-20”,表示从5分到20分钟每分钟运行一次任务
/设置频率或间隔, 如"1/15"表示从1分开始,每隔15分钟运行一次任务
L用于每月,或每周,表示每月的最后一天,或每个月的最后星期几 例如"6L"表示"每月的最后一个星期五"
W表示离给定日期最近的工作日 例如"15W"放在每月(day-of-month)上表示"离本月15日最近的工作日"
#表示该月第几个周X。例如”6#3”表示该月第3个周五
举个例子

- 说明:一般日和周的值不同时设置,如果其中一个设置了,另一个用?表示。
分钟小时
00912102022
   这些具体的细节,我们就不用自己去手写,因为这个cron表达式,有在线生成器:https://cron.qqe2.com/可以直接在这个网站上面,只要根据自己的要求去生成corn表达式即可,一般就不用自己去编写这个表达式。
   
**cron表达式案例:**
*/5 * * * * ?   每隔5秒执行一次
0 */1 * * * ?   每隔1分钟执行一次
0 0 5-15 * * ?  每天5-15点整点触发
0 0/3 * * * ?   每三分钟触发一次
0 0-5 14 * * ?  在每天下午2点到下午2:05期间的每1分钟触发 
0 0/5 14 * * ?  在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ?  在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0/30 9-17 * * ?  朝九晚五工作时间内每半小时
0 0 10,14,16 * * ? 每天上午10点,下午2点,4点 
# 使用步骤

1). 导入maven坐标 spring-context

2). 启动类添加注解 @EnableScheduling 开启任务调度

3). 自定义定时任务类
编写定时任务类:在sky-server模块中
    
/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class MyTask {

    /**
     * 定时任务 每隔5秒触发一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    //@Scheduled(fixedRate = 5000)
    public void executeTask(){
        System.out.println("定时任务开始执行:" + new Date());
    }
}

4、WebSocket全双工通信

   WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
   
**HTTP协议和WebSocket协议对比:**

- HTTP是短连接;WebSocket是长连接
- HTTP通信是单向的,基于请求响应模式;WebSocket支持双向通信
- 相同之处:HTTP和WebSocket底层都是TCP连接
- 服务器可以主动向客户端推送消息。

WebSocket缺点:

- 服务器长期维护长连接需要一定的成本
- WebSocket 是长连接,受网络限制比较大,需要处理好重连
- 各个浏览器支持程度不一

WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
 1). 直播弹幕
 2). 网页聊天
 3). 体育实况更新
 4). 股票基金报价实时更新
**入门案例**

- 需求:
- 实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。

实现步骤:

1). 直接使用websocket.html页面作为WebSocket客户端

2). 导入WebSocket的maven坐标

3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信

4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件

5). 导入定时任务类WebSocketTask,定时向客户端推送数据

1). 定义websocket.html页面


<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    
    function abc() {
        //code
    }
    
    var websocket = null;
    //Math.random()返回一个0(包含)到 1(不包含)的浮点数
    //toString(32): 转成字符串
    //substr(): 从指定位置开始截取字符串
    var clientId = Math.random().toString(36).substr(2);

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data); //event.data是服务器返回的数据
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件:
    //当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
	
	//关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>
2). 导入maven坐标

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
3). 定义WebSocket服务端组件
    
/**
 * WebSocket服务
 */
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}    
4). 定义配置类,注册WebSocket的服务端组件
    
/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}
5). 定义定时任务类,定时向客户端推送数据

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + LocalTime.now());
    }
}

5、Apache ECharts

   Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。
   
- 官网地址:https://echarts.apache.org/zh/index.html

常见效果展示:
1). 柱状图-bar 
2). 饼图-pie
3). 折线图-line

- 总结:不管是哪种形式的图形,最本质的东西实际上是数据,它其实是对数据的一种可视化展示。

- Apache Echarts官方提供的快速入门:https://echarts.apache.org/handbook/zh/get-started/

实现步骤:

1). 引入echarts.js 文件

2). 为 ECharts 准备一个设置宽高的 DOM

3). 初始化echarts实例

4). 指定图表的配置项和数据

5). 使用指定的配置项和数据显示图表
代码开发:

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>ECharts</title>
    <!-- 引入刚刚下载的 ECharts 文件 -->
    <script src="echarts.js"></script>
  </head>
  <body>
    <!-- 为 ECharts 准备一个定义了宽高的 DOM -->
    <div id="main" style="width: 600px;height:400px;"></div>
    <script type="text/javascript">
      // 基于准备好的dom,初始化echarts实例
      var myChart = echarts.init(document.getElementById('main'));

      // 指定图表的配置项和数据
      var option = {
        title: {
          text: 'ECharts 入门示例'
        },
        tooltip: {},
        legend: {
          data: ['销量']
        },
        xAxis: {
          data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
        },
        yAxis: {},
        series: [
          {
            name: '销量',
            type: 'bar',
            data: [5, 20, 36, 10, 10, 20]
          }
        ]
      };

      // 使用刚指定的配置项和数据显示图表。
      myChart.setOption(option);
    </script>
  </body>
</html>
- 总结:使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。

6、Apache POI

    Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目,主要任务是创建和维护Java API,以基于Office Open XML标准(OOXML)和Microsoft的OLE 2复合文档格式(OLE2)处理各种文件格式, 该API的组件列表如下:
API作用
POIFS该组件是所有其他POI元素的基本因素。 它用于显式读取不同的文件。
HSSF用于读取和写入MS-Excel文件的xls格式。
XSSF用于MS-Excel的xlsx文件格式。
SXSSF用于比较大的xlsx文件格式。
HWPF用于读写MS-Word的doc扩展文件。
XWPF用于读写MS-Word的docx扩展文件。
HSLF用于阅读,创建和编辑PowerPoint演示文稿。
HDGF它包含MS-Visio二进制文件的类和方法。
HPBF用于读写MS-Publisher文件。
    简单来说就是,我们可以使用 POI 在 Java 中对Miscrosoft Office各种文件进行读写操作。一般情况下,POI 都是用于操作 Excel 文件。
    
**Apache POI 的应用场景:**

- 银行网银系统导出交易明细

- 各种业务系统导出Excel报表

- 批量导入业务数据

** 入门案例 **
Apache POI既可以将数据写入Excel文件,也可以读取Excel文件中的数据。
Apache POI的maven坐标:

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
</dependency>
1、将数据写入Excel文件
    
public class POITest {

    /**
     * 基于POI向Excel文件写入数据
     * @throws Exception
     */
    public static void write() throws Exception{
        //在内存中创建一个Excel文件对象
        XSSFWorkbook excel = new XSSFWorkbook();
        //创建Sheet页
        XSSFSheet sheet = excel.createSheet("itcast");

        //在Sheet页中创建行,0表示第1行
        XSSFRow row1 = sheet.createRow(0);
        //创建单元格并在单元格中设置值,单元格编号也是从0开始,1表示第2个单元格
        row1.createCell(1).setCellValue("姓名");
        row1.createCell(2).setCellValue("城市");

        XSSFRow row2 = sheet.createRow(1);
        row2.createCell(1).setCellValue("张三");
        row2.createCell(2).setCellValue("北京");

        XSSFRow row3 = sheet.createRow(2);
        row3.createCell(1).setCellValue("李四");
        row3.createCell(2).setCellValue("上海");

        FileOutputStream out = 
            new FileOutputStream(new File("D:\\itcast.xlsx"));
        //通过输出流将内存中的Excel文件写入到磁盘上
        excel.write(out);

        //关闭资源
        out.flush();
        out.close();
        excel.close();
    }
    public static void main(String[] args) throws Exception {
        write();
    }
}
4).设置表头加粗【扩展】
    
//给表头字体加粗 -> 单元格样式
XSSFCellStyle cellStyle = workbook.createCellStyle();//产生一个单元格样式对象
XSSFFont font = workbook.createFont(); //产生一个字体对象
font.setBold(true); //加粗
cellStyle.setFont(font);

XSSFCell cell1 = row1.createCell(1);
cell1.setCellValue("姓名");
cell1.setCellStyle(cellStyle); //给当前单元格设置一个样式

XSSFCell cell2 = row1.createCell(2);
cell2.setCellValue("城市");
cell2.setCellStyle(cellStyle); //给当前单元格设置一个样式
2、读取Excel文件中的数据
    
1). 代码开发
    
    /**
     * 基于POI读取Excel文件
     * @throws Exception
     */
    public static void read() throws Exception{
        FileInputStream in = new FileInputStream(new File("D:\\itcast.xlsx"));
        //通过输入流读取指定的Excel文件
        XSSFWorkbook excel = new XSSFWorkbook(in);
        //获取Excel文件的第1个Sheet页
        XSSFSheet sheet = excel.getSheetAt(0);

        //获取Sheet页中的最后一行的行号
        int lastRowNum = sheet.getLastRowNum();

        for (int i = 0; i <= lastRowNum; i++) {
            //获取Sheet页中的行
            XSSFRow titleRow = sheet.getRow(i);
            //获取行的第2个单元格
            XSSFCell cell1 = titleRow.getCell(1);
            //获取单元格中的文本内容
            String cellValue1 = cell1.getStringCellValue();
            //获取行的第3个单元格
            XSSFCell cell2 = titleRow.getCell(2);
            //获取单元格中的文本内容
            String cellValue2 = cell2.getStringCellValue();

            System.out.println(cellValue1 + " " +cellValue2);
        }

        //关闭资源
        in.close();
        excel.close();
    }

    public static void main(String[] args) throws Exception {
        read();
    }

7、导出Excel报表

ReportServiceImpl实现类中实现导出运营数据报表的方法:

1、将资料中的运营数据报表模板.xlsx拷贝到项目的resources/template目录中
2、停止项目,删除target目录
    
@Autowired
private WorkspaceService workspaceService;

/**
 * 导出近30天的运营数据报表
 *
 * @param response
 **/
public void exportBusinessData(HttpServletResponse response) {
    LocalDate begin = LocalDate.now().minusDays(30);
    LocalDate end = LocalDate.now().minusDays(1);
    //查询概览运营数据,提供给Excel模板文件
    BusinessDataVO businessData = workspaceService.getBusinessData(
                                    LocalDateTime.of(begin, LocalTime.MIN),
                                    LocalDateTime.of(end, LocalTime.MAX));
    
    //需要从当前运行路径下获取excel模版文件
    InputStream inputStream = this.getClass().getClassLoader()
            .getResourceAsStream("template/运营数据报表模板.xlsx");
    try {
        //基于提供好的模板文件创建一个新的Excel表格对象
        XSSFWorkbook excel = new XSSFWorkbook(inputStream);
        //获得Excel文件中的一个Sheet页
        XSSFSheet sheet = excel.getSheet("Sheet1");

        //在第2行,第2列设置统计日期:2026-05-01至2026-05-30
        sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end);

        //获得第4行
        XSSFRow row = sheet.getRow(3);

        //在第3列设置:营业额
        row.getCell(2).setCellValue(businessData.getTurnover());
        //在第5列设置:订单完成率
        row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
        //在第7列设置:新增用户数
        row.getCell(6).setCellValue(businessData.getNewUsers());

        //获取第5行
        row = sheet.getRow(4);
        //在第3列设置:有效订单
        row.getCell(2).setCellValue(businessData.getValidOrderCount());
        //在第5列设置:平均客单价
        row.getCell(4).setCellValue(businessData.getUnitPrice());

        //写入明细数据(按天显示)
        for (int i = 0; i < 30; i++) {
            LocalDate date = begin.plusDays(i);
            //准备订单明细数据
            businessData = workspaceService.getBusinessData(
                            LocalDateTime.of(date, LocalTime.MIN),
                            LocalDateTime.of(date, LocalTime.MAX));
            //从第8行开始写入
            row = sheet.getRow(7 + i);
            row.getCell(1).setCellValue(date.toString());
            row.getCell(2).setCellValue(businessData.getTurnover());
            row.getCell(3).setCellValue(businessData.getValidOrderCount());
            row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
            row.getCell(5).setCellValue(businessData.getUnitPrice());
            row.getCell(6).setCellValue(businessData.getNewUsers());
        }

        //通过输出流将文件下载到客户端浏览器中
        ServletOutputStream out = response.getOutputStream();
        excel.write(out);
        //关闭资源
        out.flush();
        out.close();
        excel.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}
  • 23
    点赞
  • 252
    收藏
    觉得还不错? 一键收藏
  • 9
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值