前言
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、技术选型
关于本项目的技术选型, 我们将会从 用户层、网关层、应用层、数据层 这几个方面进行介绍,主要用于展示项目中使用到的技术框架和中间件等。
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)数据库环境搭建
在数据库中创建如下表格
序号 | 表名 | 中文名 |
---|---|---|
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 | 订单明细表 |
建表规范:(阿里开源的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.Driverhost: localhostport: 3306database: sky_take_outusername: 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 {@Autowiredprivate 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存储到ThreadLocalBaseContext.setCurrentId(empId);//3、通过,放行return true;} catch (Exception ex) {//......................}}@Override//请求结束之前执行,清理ThreadLocal存储的IDpublic 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即可
3、com.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("扩展消息转换器...");//创建一个消息转换器对象,注意不要导错包是Jackson2HttpMappingJackson2HttpMessageConverter 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 INSERTOperationType 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 {//通过方法名和参数类型获取定义好的方法:setCreateTimeMethod 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.comaccess-key-id: LTAI5tPeFLzsPPT8gG3LPW64access-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/ObjectNameStringBuilder 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 {@Autowiredprivate AliOssUtil aliOssUtil;/*** 文件上传* @param file* @return*/@PostMapping("/upload")@ApiOperation("文件上传")public Result<String> upload(MultipartFile file){log.info("文件上传:{}",file);try {//原始文件名String originalFilename = file.getOriginalFilename();//截取原始文件名的后缀:abc.dfdfdf.pngString 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数据库。NoSql(Not 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(项目中使用)
Redis的Windows版属于**绿色软件**,直接解压即可使用。
redis-cli.exe: Redis客户端redis-client2)在Linux中安装Redis(简单了解)> - 后续学完Linux课程再操作
> - mac系统安装和Linux一样
> - mac不想自己编译安装,参考:https://blog.csdn.net/realize_dream/article/details/106227622在Linux系统安装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.confRedis服务默认端口号为 **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种类型>();1、new HashMap<String, String>();
2、new HashMap<String, new HashMap<>>();
3、new HashMap<String, new ArrayList<>>();
4、new HashMap<String, new HashSet<>>();
5、new 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 的值设为 valuehset: 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: rpushLRANGE 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 RedisSpring 对 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: localhostport: 6379#设置密码,没有设置则注释掉password: 123456database: 0
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15
#可以使用select 切换不同的数据库
select 0
select 1
在application.yml中添加读取application-dev.yml中的相关Redis配置spring:profiles:active: devredis:host: ${sky.redis.host}port: ${sky.redis.port}password: ${sky.redis.password}database: ${sky.redis.database}
3). 编写配置类,创建RedisTemplate对象@Configuration
@Slf4j
public class RedisConfiguration {@Beanpublic 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 {@Autowiredprivate 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 nameString 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);//降序:进行反转reversezset1 = zSetOperations.reverseRange("zset1", 0, -1);System.out.println(zset1);
6). 通用命令操作Set keys = redisTemplate.keys("*");System.out.println(keys);//是否存在:haveBoolean 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_开头的keycleanCache("dish_*");
修改菜品优化//==========================新增代码=====================cleanCache("dish_" + dishDTO.getCategoryId());
菜品起售停售优化
//==========================新增代码=====================//将所有的菜品缓存数据清理掉,所有以dish_开头的keycleanCache("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: rootpassword: rootredis:host: localhostport: 6379password: 123456
2). @CachePut注解@CachePut 说明: 作用: 将方法返回值,放入缓存 value: 缓存的名称, 每个缓存名称下面可以有很多key key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法在save方法上加注解@CachePut
当前UserController的save方法是用来保存用户信息的,在该用户信息保存到数据库的同时,也往缓存中缓存一份数据,我们可以在save方法上加上注解 @CachePut,用法如下:/*CachePut:将方法返回值放入缓存value:缓存的名称,每个缓存名称下面可以有多个keykey:缓存的key*/@PostMapping//@CachePut(value = "userCache", key = "#resutl.id")@CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1public 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
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
- deleteHttpClient发送请求步骤:- 创建HttpClient对象
- 创建Http请求对象
- 调用HttpClient的execute方法发送请求其他:OkHttp、RestTemplate(SpringMVC)……应该导入HttpClient相关的坐标
<dependency><groupId>com.aliyun.oss</groupId><artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<!--阿里云上传:底层是用HttpClient将上传的请求发送到阿里云服务器-->
1、GET方式请求@SpringBootTest
public class HttpClientTest {/*** 测试通过httpclient发送GET方式的请求*/@Testpublic 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=step12). 完善小程序信息登录小程序后台:https://mp.weixin.qq.com/完善小程序信息、小程序类目,不能选择小游戏类目,查看小程序的 AppID和 AppSecret3). 下载开发者工具其他系统下载地址: 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:使用自己的AppID3). 查看项目结构
主体的文件: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.html1. 小程序端,调用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和secretwechat:appid: wxffb3637a228223b8secret: 84311df9199ecacdf4f12d27b6b9522d
application.yml
#sky:wechat:appid: ${sky.wechat.appid}secret: ${sky.wechat.secret}
配置为微信用户生成jwt令牌时使用的配置项:
application.yml
#sky:#jwt:# 省略......user-secret-key: itheimauser-ttl: 7200000user-token-name: authentication
用户端拦截器/*** jwt令牌校验的拦截器*/
@Component
@Slf4j
public class JwtTokenUserInterceptor implements HandlerInterceptor {@Autowiredprivate 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.HandlerMethodif (!(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-started2). cpolar指定authtoken
复制authtoken:https://dashboard.cpolar.com/auth执行命令:
#Windows:
cpolar.exe authtoken MmJiMTBiZDAtMz333330ZWI5LTlhOTQtODE1ZjcxNmZhOGRl3). 获取临时域名执行命令:cpolar.exe http 80804). 验证临时域名有效性启动项目,访问接口文档:http://localhost:8080/doc.html 使用临时域名访问,证明临时域名生效
**微信支付相关配置**#sky:#wechat:#appid: wxffb3637a228223b8#secret: 84311df9199ecacdf4f12d27b6b9522d#支付需要用到的配置mchid : 1561414331 #支付商户号mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606 #商户编号apiV3Key: CZBK51236435wxpay435434323FFDuv3privateKeyFilePath: D:\apiclient_key.pemweChatPayCertFilePath: D:\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem#设置微信支付回调接口notifyUrl: http://5c968a9a.r1.cpolar.top/notify/paySuccess#退款回调接口refundNotifyUrl: https://www.weixin.qq.com/wxpay/refundapplication.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; //小程序的appidprivate 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 {@Autowiredprivate OrderService orderService;@Autowiredprivate 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个周五 |
举个例子- 说明:一般日和周的值不同时设置,如果其中一个设置了,另一个用?表示。
秒 | 分钟 | 小时 | 日 | 月 | 周 | 年 |
---|---|---|---|---|---|---|
0 | 0 | 9 | 12 | 10 | ? | 2022 |
这些具体的细节,我们就不用自己去手写,因为这个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-context2). 启动类添加注解 @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);//判断当前浏览器是否支持WebSocketif('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();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("sid") String sid) {System.out.println("客户端:" + sid + "建立连接");sessionMap.put(sid, session);}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, @PathParam("sid") String sid) {System.out.println("收到来自客户端:" + sid + "的信息:" + message);}/*** 连接关闭调用的方法** @param sid*/@OnClosepublic 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 {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
5). 定义定时任务类,定时向客户端推送数据@Component
public class WebSocketTask {@Autowiredprivate 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 准备一个设置宽高的 DOM3). 初始化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-30sheet.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();}
}