NEWS LETTER

苍穹外卖

Scroll down

软件开发整体介绍

软件开发流程

image-20250529215759695

角色分工

  • 项目经理:对整个项目负责,任务分配、把控进度
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等
  • UI设计师:根据产品原型输出界面效果图
  • 架构师:项目整体架构设计、技术选型等
  • 开发工程师:代码实现
  • 测试工程师:编写测试用例,输出测试报告
  • 运维工程师:软件环境搭建、项目上线

软件环境

  • 开发环境(development):开发人员在开发阶段使用的环境,一般外部用户无法访问
  • 测试环境(testing):专门给测试人员使用的环境,用于测试项目,一般外部用户无法访问
  • 生产环境(production):即线上环境,正式提供对外服务的环境

项目

项目介绍

image-20250531115631711

image-20250531115729943

产品原型

页面效果

image-20250531120328780

技术选型

image-20250531120509711

开发环境搭建

前端环境

image-20250531132330179

后端环境

image-20250531132430665

image-20250531133234255

image-20250531135453218

image-20250531135516872

image-20250531135933719

Git

后端环境搭建-使用Git进行版本控制
使用Git进行项目代码的版本控制,具体操作:

  • 创建Git本地仓库
  • 创建Git远程仓库
  • 将本地文件推送到Git远程仓库

数据库环境

image-20250531172230786

前后端联调

image-20250531181636895

思考

场景描述

  • 前端请求地址http://localhost/api/employee/login(POST请求)
  • 后端接口地址http://localhost:8080/admin/employee/login

后端接口定义(EmployeeController)

1
2
3
4
5
6
7
8
9
10
11
12
Java@RestController  
@RequestMapping("/admin/employee") // 类级别的请求路径前缀
public class EmployeeController {

@Autowired
private EmployeeService employeeService;

@PostMapping("/login") // 方法级别的请求路径
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
// 登录逻辑...
}
}
1
2
3
4
5
Request URLhttp://localhost/api/employee/login
Request Method:POST
Status Code:200
Remote Address: 127.0.0.1:80
Referrer Policy: strict-origin-when-cross-origin

Nginx

image-20250531182409592

image-20250531182707314

nginx反向代理的配置方式:

1
2
3
4
5
6
7
server{
listen 80;
server_namelocalhost;
location /api/ {
proxy_passshttp://localhost:8080/admin/;#反向代理
}
}

nginx负载均衡的配置方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. 定义上游服务器集群(负载均衡池)  
upstream webservers {
server 192.168.100.128:8080; # 后端服务器1
server 192.168.100.129:8080; # 后端服务器2
}

# 2. 配置虚拟主机
server {
listen 80; # 监听端口
server_name localhost; # 域名

# 3. 路径匹配与负载均衡转发
location /api/ { # 匹配以 /api/ 开头的请求
proxy_pass http://webservers/admin/; # 转发到上游集群,路径重写(/api/ → /admin/)
}
}

nginx负载均衡策略:

策略名称 说明
轮询 默认方式,请求按顺序轮流分配到后端服务器
weight 权重方式(默认权重为 1),权重越高,被分配的请求越多
ip_hash 依据客户端 IP 分配请求,固定 IP 始终访问同一后端服务器(解决会话保持问题)
least_conn 依据后端服务器连接数分配,优先将请求分配给当前连接数最少的服务器
url_hash 依据请求 URL 分配,相同 URL 始终访问同一后端服务器(适用于缓存场景)
fair 依据后端服务器响应时间分配,响应时间越短的服务器被优先分配请求

完善登录功能(加密密码)

image-20250531184714608

完善登录功能

核心步骤

  1. 数据库密码加密:将数据库中存储的明文密码,改为 MD5加密后的密文(不可逆加密,增强安全性)。
  2. 代码逻辑调整:前端提交的密码,需先进行 MD5加密,再与数据库中的密文密码比对。

关键代码实现(密码比对逻辑)

1
2
3
4
5
6
7
8
// 1. 对前端提交的密码进行MD5加密(转为16进制字符串)  
password = DigestUtils.md5DigestAsHex(password.getBytes());

// 2. 与数据库中的密文密码比对(employee为数据库查询到的用户对象)
if (!password.equals(employee.getPassword())) {
// 密码错误,抛出自定义异常(如"密码错误")
throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
}

导入接口文档

image-20250531185821661

用yapi导入接口文档

Swagger

介绍

1. Swagger

  • 核心功能:按照规范定义接口及相关信息,自动生成接口文档,并提供在线接口调试页面(无需第三方工具即可测试接口)。
  • 官网https://swagger.io/

2. Knife4j

  • 定位:为 Java MVC 框架(如 Spring Boot)集成 Swagger 的增强解决方案,提供更友好的 UI 界面和更丰富的功能(如接口排序、导出文档等)。

3. Maven 依赖(Knife4j 起步依赖)

1
2
3
4
5
XML<dependency>  
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>

使用方式

  1. 导入Knife4j的Maven坐标
  2. 在配置类中加入Knife4j相关配置
  3. 设置静态资源映射(否则接口文档页面无法访问)

配置类示例(WebMvcConfiguration)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean  
public Docket docket() {
// 1. 配置API文档基本信息
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档") // 文档标题
.version("2.0") // 版本
.description("苍穹外卖项目接口文档") // 描述
.build();

// 2. 创建Docket对象(指定文档类型为SWAGGER_2)
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo) // 关联ApiInfo
.select()
// 指定生成接口需要扫描的包(Controller层)
.apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
.paths(PathSelectors.any()) // 匹配所有路径
.build();

return docket;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**  
* 设置静态资源映射(确保Knife4j接口文档页面可访问)
* @param registry 资源处理器注册器
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始设置静态资源映射...");

// 1. 映射Knife4j接口文档页面(doc.html)
registry.addResourceHandler("/doc.html")
.addResourceLocations("classpath:/META-INF/resources/");

// 2. 映射WebJars静态资源(Knife4j依赖的CSS、JS等)
registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/");
}

通过这个功能可以查看接口文档和测试

思考

通过Swagger就可以生成接口文档,那么我们就不需要Yapi了?

  1. Yapi是设计阶段使用的工具,管理和维护接口
  2. Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

常用注解

通过注解可以控制生成的接口文档,使接口文档拥有更好的可读性,常用注解如下:

注解 说明
@Api 用在类上(如Controller),对类进行说明(如接口模块名称、功能描述)。
@ApiModel 用在类上(如Entity、DTO、VO),描述数据模型的含义(如“用户登录请求DTO”)。
@ApiModelProperty 用在属性上,描述属性的含义、数据类型、是否必填等(如“用户名,长度2-20字符”)。
@ApiOperation 用在方法上(如Controller的接口方法),说明方法的用途、作用(如“用户登录接口,返回JWT令牌”)。

新增员工

需求分析和设计

image-20250531194342483

image-20250531194747536

数据库设计(employee表)

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 姓名
username varchar(32) 用户名 唯一
password varchar(64) 密码
phone varchar(11) 手机号
sex varchar(2) 性别
id_number varchar(18) 身份证号
status int 账号状态 1正常 0锁定
create_time datetime 创建时间
update_time datetime 最后修改时间
create_user bigint 创建人id
update_user bigint 最后修改人id

代码

image-20250531195711620

1
2
3
4
5
6
7
8
9
10
@Data
public class EmployeeDTO implements Serializable{
private Long id;
private String username;
private String name;
private String phone;
private String sex;
private String idNumber;
}

测试(视频多看)

完善

程序存在的问题:

  • 录入的用户名已存在,抛出异常后没有处理
  • 新增员工时,创建人id和修改人id设置为了固定值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
Log.error("异常信息:{}", ex.getMessage());
String message = ex.getMessage();
if (message.contains("Duplicate key")) {
// 用空格来分割
String[] split = message.split(" ");
String username = split[2];
String msg = username + MessageConstant.ALREADY_EXISTS;
return Result.error(msg);
} else {
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}

1.从请求头获取并解析JWT令牌,前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Java// JwtTokenAdminInterceptor 拦截器中的核心逻辑  
// 1. 从请求头获取令牌(令牌名称从配置文件读取,如 "token")
String token = request.getHeader(jwtProperties.getAdminTokenName());

// 2. 校验令牌并解析员工ID
try {
// 调用工具类解析JWT令牌(需传入密钥和令牌字符串)
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
// 从令牌的Claims中提取员工ID(EMP_ID为自定义常量,对应生成令牌时存入的键)
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
// 3. 解析成功,放行请求
return true;
} catch (Exception ex) {
// 4. 解析失败(令牌无效/过期),返回401未授权
response.setStatus(401);
return false;
}

2. 问题:解析出的员工ID如何传递给Service的save方法?

3.代码完善 Threadlocal

  • ThreadLocal并不是一个Thread,而是Thread的局部变量。
  • ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不
  • 能访问。

员工分页查询

需求分析和设计

image-20250531225336195

image-20250531225622358

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// 1. 设置分页参数(当前页码、每页记录数)
PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());

// 2. 执行分页查询(Mapper层返回Page对象,包含分页数据)
Page<Employee> page = employeeMapper.pageQuery(employeePageQueryDTO);

// 3. 提取分页结果(总记录数 + 当前页数据列表)
long total = page.getTotal(); // 总记录数
List<Employee> records = page.getResult(); // 当前页数据列表

// 4. 封装并返回分页结果对象
return new PageResult(total, records);
}

完善

时间格式

方式一:在属性上加入注解,对日期进行格式化

1
2
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
privateLocalDateTimeupdateTime;

方式二:在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式化处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**  
* 扩展MVC框架的消息转换器(用于统一处理日期格式化等)
* @param converters 消息转换器列表
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("开始扩展消息转换器...");

// 1. 创建自定义消息转换器(基于Jackson)
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

// 2. 设置对象转换器(自定义Jackson配置,如日期格式化)
converter.setObjectMapper(new JacksonObjectMapper()); // JacksonObjectMapper为自定义类,封装日期格式化规则

// 3. 将自定义转换器添加到转换器列表(添加到首位,优先生效,覆盖默认转换器)
converters.add(0, converter);
}

启用禁用员工账号

需求分析和设计

业务规则:
可以对状态为“启用”的员工账号进行“禁用”操作
可以对状态为“禁用”的员工账号进行“启用”操作
状态为“禁用”的员工账号不能登录系统

image-20250603110427821

代码

完善

编辑员工

需求分析和设计

代码

1
2
3
4
5
6
7
8
@override
public void update(EmployeeDT0 employeeDTO){
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTo, employee);
employee.setUpdateTime(LocalDateTime.now());
employee.setUpdateUser(BaseContext.getCurrentId()):
employeeMapper.update(employee);
}

导入分类模块功能代码

需求分析和设计

业务规则:

  • 分类名称必须是唯一
  • 分类按照类型可以分为菜品分类套餐分类
  • 新添加的分类状态默认为**“禁用”**

接口设计:

  • 新增分类
  • 分类分页查询
  • 根据id删除分类
  • 修改分类
  • 启用禁用分类
  • 根据类型查询分类

image-20250603152746332

公共字段自动填充

问题分析

1. 公共字段定义

序号 字段名 含义 数据类型
1 create_time 创建时间 datetime
2 create_user 创建人ID bigint
3 update_time 修改时间 datetime
4 update_user 修改人ID bigint

2. 问题:代码冗余(传统方式)

在新增/修改业务数据时,需重复设置公共字段(以员工、分类为例):

1
2
3
4
5
6
7
8
9
10
11
Java// 员工新增时设置公共字段  
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
employee.setCreateUser(BaseContext.getCurrentId()); // 从ThreadLocal获取当前登录人ID
employee.setUpdateUser(BaseContext.getCurrentId());

// 分类新增时设置公共字段(重复代码)
category.setCreateTime(LocalDateTime.now());
category.setUpdateTime(LocalDateTime.now());
category.setCreateUser(BaseContext.getCurrentId());
category.setUpdateUser(BaseContext.getCurrentId());

问题:每个实体类的新增/修改方法都需重复编写上述代码,导致冗余、维护成本高。

3. 解决思路

通过 AOP切面MyBatis拦截器 统一处理公共字段赋值,避免重复代码。

实现思路

序号 字段名 含义 数据类型 操作类型(何时填充)
1 create_time 创建时间 datetime insert(新增时)
2 create_user 创建人ID bigint insert(新增时)
3 update_time 修改时间 datetime insert、update(新增/修改时)
4 update_user 修改人ID bigint insert、update(新增/修改时)
  • 自定义注解AutoFill,用于标识需要进行公共字段自动填充的方法
  • 自定义切面类AutoFillAspect,统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
  • 在Mapper的方法上加入AutoFill注解

新增菜品

需求分析和设计

业务规则:

  • 菜品名称必须是唯一的
  • 菜品必须属于某个分类下,不能单独存在
  • 新增菜品时可以根据情况选择菜品的口味
  • 每个菜品必须对应一张图片

接口设计:

  • 根据类型查询分类(已完成)
  • 文件上传

新增菜品

image-20250604101429974

代码

有点复杂,记得多看看视频

菜品分页查询

需求分析和设计

业务规则:

  • 根据页码展示菜品信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询

image-20250604163738728

代码

代码与之前的分页查询一样,注意Pagehelper的用法

删除菜品

需求分析和设计

业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除掉

image-20250605084032261

image-20250605084219125

只需要写一个接口,即批量删除接口。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
     /**
* 批量删除菜品
* @param ids
*/
@Transactional
@Override
public void deleteBatch(List<Long> ids) {
log.info("批量删除菜品,ids:{}", ids);
//判断当前菜品是否在售,ids中是否存在在售的菜品,如果在售则不能删除
for (Long id : ids) {
Dish dish = dishMapper.getById(id);
if(dish.getStatus() == StatusConstant.ENABLE){
//当前有菜品在售,不能删除
throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
}
}
//判断当前菜品是否关联套餐,ids中存在关联套餐的菜品则不能删除
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);
// }
//根据菜品id集合删除菜品表里的菜品数据
dishMapper.deleteByIds(ids);
//删除菜品关联的口味数据
dishFlavorMapper.deleteByDishIds(ids);
}

修改菜品

需求分析和设计

回写信息

image-20250629222630939

image-20250629222752609

image-20250629222832441

代码

套餐管理

useGeneratedKeys=”true”和keyProperty=”id”这两个属性对于MyBatis在插入数据后将数据库生成的ID回填到Java对象中至关重要。
以下是它们各自的作用:

  1. useGeneratedKeys=”true”:这个属性告诉MyBatis,数据库会为这次插入操作生成一个主键。当设置为true时,MyBatis会尝试获取这个由数据库生成的主键。
  2. keyProperty=”id”:这个属性指定了输入参数对象(在本例中是Setmea1对象)中哪个属性应该被更新为数据库生成的主键。 id 表示 Setmeal 类的 id 属性。
  • 如果没有这两个属性,MyBatis会执行一个普通的插入操作,但它不会期望或尝试去获取任何数据库自动生成的主键。因此,在插入操作完成后,传递给insert方法的Setmeal对象的id字段将保持其初始值(通常是nul1或0),因为 MyBatis 并不知道要去获取并设置这个生成的主键。
  • 简单来说,这两个属性共同协作,使MyBatis能够感知到数据库的自动增长主键机制,并在插入成功后,将数据库生成的主键值“回填”到你的Java对象中,这样你就可以在后续操作中立即使用这个ID。
1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.sky.mapper.SetmealMapper">

<!-- 插入套餐数据,并回填数据库生成的自增ID -->
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
insert into setmeal (name, category_id, price, image, status, description)
values (#{name}, #{categoryId}, #{price}, #{image}, #{status}, #{description})
</insert>

</mapper>

注意事务和数据库关系

Redis

介绍

image-20250702113540361

下载

image-20250702113650562

image-20250702113918422

发布 ·TPORADOWSKI/REDIS (github.com)

启动与注册window服务

在命令行里输入

1
redis-server.exe redis.windows.conf

在Redis的目录下输入:

1
redis-server --service-install redis.windows.conf --service-name RedisService --port 6379

1
redis-server --service-start --service-name RedisService

显示:

1
Redis service successfully started

即为成功

这样设置完后,就可以让Redis服务开机自启

Redis数据类型

5种常用的数据类型

Redis存储的是key-value结构的数据,其中key是字符串类型,value有s种常用的数据类型:

  • 字符串 string
  • 哈希 hash
  • 列表list
  • 集合 set
  • 有序集合sorted set/zset

image-20250702223624244

常用命令

字符串操作命令

命令格式 说明
SET key value 设置指定 key 的值
GET key 获取指定 key 的值
SETEX key seconds value 设置指定 key 的值,并将 key 的过期时间设为 seconds
SETNX key value 只有在 key 不存在时设置 key 的值

哈希操作命令

Redis Hash 是一个 String 类型的 field 和 value 的映射表,特别适合存储对象(如用户信息、商品属性等)。

命令格式 说明
HSET key field value 将哈希表 key 中的字段 field 的值设为 value
HGET key field 获取哈希表 key 中指定字段 field 的值
HDEL key field 删除哈希表 key 中的指定字段 field
HKEYS key 获取哈希表 key 中的所有字段名(field)
HVALS key 获取哈希表 key 中的所有字段值(value)

Hash 结构示意图

1
2
3
4
key  
├─ field1 → value1
├─ field2 → value2
└─ ...

(一个 key 对应多个 field-value 键值对,类似 JSON 对象)

列表操作命令

Redis 列表是简单的字符串列表,按照插入顺序排序,可模拟栈、队列等数据结构。

命令格式 说明
LPUSH key value1 [value2] 将一个或多个值插入到列表头部(左侧)
LRANGE key start stop 获取列表中指定范围的元素(start 起始索引,stop 结束索引,-1 表示最后一个元素)
RPOP key 移除并获取列表最后一个元素(右侧)
LLEN key 获取列表的长度(元素个数)

列表结构示意图

1
2
3
 key  

[a, b, c, d] # 列表元素按插入顺序排列(示例:LPUSH key d c b a 后的结果)
  • 左侧:列表头部(LPUSH 插入,LPOP 移除)
  • 右侧:列表尾部(RPUSH 插入,RPOP 移除)

image-20250703155706798

集合操作命令

Redis Set 是 String 类型的无序集合,集合成员唯一(不允许重复),支持交集、并集等集合操作。

命令格式 说明
SADD key member1 [member2] 向集合添加一个或多个成员
SMEMBERS key 返回集合中的所有成员(无序)
SCARD key 获取集合的成员数( cardinality )
SINTER key1 [key2] 返回给定所有集合的交集(共同成员)
SUNION key1 [key2] 返回所有给定集合的并集(合并成员)
SREM key member1 [member2] 删除集合中一个或多个成员

Set 结构示意图

1
2
3
 key  

{ a, b, c } # 无序集合,成员唯一(重复添加会自动去重)
  • 无序性:成员存储顺序与插入顺序无关,SMEMBERS 返回结果不固定;
  • 唯一性:集合中不会有重复成员(SADD 重复成员会忽略)。

有序集合操作命令

Redis 有序集合(Sorted Set)是 String 类型元素的集合,不允许重复成员,每个元素关联一个 double 类型的分数(score),并按分数从小到大排序。

命令格式 说明
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员(指定分数 score
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员(从小到大排序),WITHSCORES 可携带分数
ZINCRBY key increment member 为有序集合中指定成员的分数加上增量 increment(可正可负)
ZREM key member [member ...] 移除有序集合中的一个或多个成员

有序集合结构示意图

1
2
3
4
5
6
7
 key  

{
a: 0.1, // 成员a,分数0.1
c: 2.5, // 成员c,分数2.5
b: 3.0 // 成员b,分数3.0(按分数升序排序:a → c → b)
}
  • 有序性:成员按分数(score)从小到大排序,可通过索引快速获取排名区间的成员;
  • 唯一性:成员不可重复,若添加已存在的成员,会更新其分数并重新排序。

image-20250703175413701

分数越大排得越前

通用命令

命令格式 说明
KEYS pattern 查找所有符合给定模式(pattern)的 key(如 KEYS user:* 匹配所有以 user: 开头的 key)
EXISTS key 检查给定 key 是否存在(返回 1 存在,0 不存在)
TYPE key 返回 key 所储存的值的数据类型(如 string、hash、list 等)
DEL key 删除指定 key(若 key 存在则删除,返回删除的 key 数量)
  • pattern:正则表达式

例子:

1
2
3
keys set*
set1
set2
  • del可以批量删除
1
2
> del set1 set2 zset1
3

在Java中使用Redis

Redis的Java客户端

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

  • Jedis
  • Lettuce
  • Spring Data Redis

SpringDataRedis是Spring的一部分,对Redis底层开发包进行了高度封装。
在Spring项目中,可以使用SpringDataRedis来简化操作。

Spring Data Redis

使用方式

操作步骤:

  1. 导入SpringDataRedis的maven坐标

    1
    2
    3
    4
    5
    <!-- pom.xml -->  
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    # application.yml  
    spring:
    redis:
    host: localhost # Redis服务器地址
    port: 6379 # Redis端口(默认6379)
    password: 123456 # Redis密码(若未设置密码可省略)
    # 可选配置:连接池、超时时间等
    lettuce:
    pool:
    max-active: 8 # 最大连接数
    max-idle: 8 # 最大空闲连接数
    min-idle: 0 # 最小空闲连接数
    timeout: 2000ms # 连接超时时间
  2. 配置Redis数据源

  3. 编写配置类,创建RedisTemplate对象

  4. 通过RedisTemplate对象操作Redis

操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Configuration  
@Slf4j
public class RedisConfiguration {

@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
log.info("开始创建redis模板类...");
RedisTemplate redisTemplate = new RedisTemplate();

// 设置Key的序列化器(默认JdkSerializationRedisSerializer,此处替换为StringRedisSerializer避免key乱码)
redisTemplate.setKeySerializer(new StringRedisSerializer());

// 设置Redis连接工厂(从Spring容器注入,包含配置文件中的redis连接信息)
redisTemplate.setConnectionFactory(redisConnectionFactory);

return redisTemplate;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
package com.sky.test;

import lombok.val;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.connection.DataType;
import org.springframework.data.redis.core.*;

import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

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

@Test
public void testRedisTemplate() {
System.out.println(redisTemplate);
ValueOperations valueOperations = redisTemplate.opsForValue();
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
}

/**
* 操作字符串类型的数据
*/
@Test
public void testRedisString() {
//set get setex setnx
redisTemplate.opsForValue().set("city", "北京");
String city = (String) redisTemplate.opsForValue().get("city");
System.out.println(city);
redisTemplate.opsForValue().set("code", "1234", 3, TimeUnit.MINUTES);
redisTemplate.opsForValue().setIfAbsent("lock", "1");
redisTemplate.opsForValue().setIfAbsent("lock", "2");
}

/**
* 操作hash类型数据
*/
@Test
public void testRedisHash() {
//hset hget hdel hkeys hvals
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("user1", "name", "张三");
hashOperations.put("user1", "age", "12");
//hget
String name = (String) hashOperations.get("user1", "name");
System.out.println(name);
//hkeys
Set keys = hashOperations.keys("user1");
System.out.println(keys);
//hvals
List values = hashOperations.values("user1");
System.out.println(values);


//hdel
hashOperations.delete("user1", "age");

}

/**
* 操作列表类型的数据
*/
@Test
public void testRedislist() {
//Lpush Lrange rpop Llen
ListOperations listOperations = redisTemplate.opsForList();
//Lpush
listOperations.leftPushAll("mylist", "a", "b", "c");
//Lpush,是有listOperations.rightPush()的
listOperations.leftPush("mylist", "d");
//Lrange
List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);
//rpop,是有listOperations.leftPop();的
listOperations.rightPop("mylist");

//Llen
Long size = listOperations.size("mylist");
System.out.println(size);
}

/**
* 操作集合类型的数据
*/
@Test
public void testRedisSet(){
// sadd smember scard sinter sunion srem
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);

Set intersect = setOperations.intersect("set1", "set2");
System.out.println(intersect);

Set union = setOperations.union("set1", "set2");
System.out.println(union);
setOperations.remove("set1","a","b");
}

/**
* 操作有序集合类型的数据
*/
@Test
public void testRedisZSet(){
////zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();

zSetOperations.add("zset1", "a", 10);
zSetOperations.add("zset1", "b", 20);
zSetOperations.add("zset1", "c", 30);

Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);

zSetOperations.incrementScore("zset1", "a", 10);
System.out.println(zset1);

zSetOperations.remove("zset1", "a");
System.out.println(zset1);
}

/**
* 通用命令操作
*/
@Test
public void testRedisCommon(){
//keys exists type del
Set keys =redisTemplate.keys( "*");
System.out.println(keys);
Boolean name=redisTemplate.hasKey("name");
Boolean set1=redisTemplate.hasKey("set1");
for (Object key : keys) {
DataType type = redisTemplate.type(key);
System.out.println(type.name());
}
redisTemplate.delete("mylist");
}
}

店铺营业状态设置

需求分析和设计

image-20250706124643946

image-20250706125039384

image-20250706125129850

代码

完善

HttpClient

介绍

就是可以再Java里面写http协议并发送

HttpClient 介绍

HttpClient 是 Apache Jakarta Common 下的子项目,提供高效、功能丰富的 HTTP 协议客户端编程工具包,支持 HTTP 协议最新版本和建议。

Maven 依赖

1
2
3
4
5
XML<dependency>  
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>

核心 API

  • HttpClient
  • HttpClients
  • CloseableHttpClient
  • HttpGet
  • HttpPost

发送请求步骤

  1. 创建 HttpClient 对象
  2. 创建 Http 请求对象(如 HttpGet/HttpPost)
  3. 调用 HttpClient 的 execute 方法发送请求

入门案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.sky.test;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;
import java.io.UnsupportedEncodingException;

@SpringBootTest
public class HttpClientTest {
/**
* 试通过httpcLient发送GET方式的请求
*/
@Test
public void testGet() throws IOException {
//创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象,HttpGet对象,设置url访问地址
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();
}

/**
* 试通过httpcLient发送POST方式的请求
*/
@Test
public void testPost() throws IOException {
//创建HttpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象,HttpPOST对象,设置url访问地址
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");

JSONObject jsonObject=new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("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();

}
}

微信小程序

介绍

image-20250709223059553

image-20250709223119899

image-20250709223136833

image-20250709223232193

准备工作

image-20250709223349295

入门案例

image-20250710155805465

image-20250710160055539

微信登陆

image-20250711152047951

需求分析

image-20250711161507491

商品浏览

需求分析

接口设计:

  • 查询分类
  • 根据分类id查询菜品
  • 根据分类id查询套餐
  • 根据套餐id查询包含的菜品

image-20250711231201440

image-20250711232233590

image-20250711232336715

image-20250711232754655

菜品缓存

问题说明

image-20250712112556891

image-20250712113506823

实现思路

image-20250712161010063

在管理端增删改的时候要进行清理缓存,以防数据不一致

缓存套餐

Spring Cache

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

Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现,例如:

  • EHCache
  • Caffeine
  • Redis

Maven 依赖

1
2
3
4
5
XML<dependency>  
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>2.7.3</version>
</dependency>

常用注解

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@PostMapping
@CachePut(cacheNames = "userCache", key = "#user.id") // 如果使用Spring Cache缓存数据,key的生成:userCache::1
public User save(@RequestBody User user){
userMapper.insert(user);
return user;
}

// 如果使用Spring Cache缓存数据,key的生成:userCache::2
@CachePut(cacheNames = "userCache", key = "#user.id")
// @CachePut(cacheNames = "userCache", key = "#result.id") // 对象导航(获取方法返回值的id)
// @CachePut(cacheNames = "userCache", key = "#p0.id") // 使用方法参数索引(p0表示第一个参数)
// @CachePut(cacheNames = "userCache", key = "#a0.id") // 使用方法参数别名(a0表示第一个参数)
// @CachePut(cacheNames = "userCache", key = "#root.args[0].id") // 使用root对象获取第一个参数的id


@GetMapping
@Cacheable(cacheNames = "userCache", key = "#id") // key的生成:userCache::10
public User getById(Long id){
User user = userMapper.getById(id);
return user;
}

@DeleteMapping
@CacheEvict(cacheNames = "userCache", key = "#id") // key的生成:userCache::10
public void deleteById(Long id){
userMapper.deleteById(id);
}

@DeleteMapping("/delAll")
@CacheEvict(cacheNames = "userCache", allEntries = true) // 删除userCache中所有缓存数据
public void deleteAll(){
userMapper.deleteAll();
}

实现思路

具体的实现思路如下:

  • 导入SpringCache和Redis相关maven坐标
  • 在启动类上加入@EnableCaching注解,开启缓存注解功能
  • 在用户端接口SetmealController的list方法上加入@Cacheable注解
  • 在管理端接口SetmealController的save、delete、update、startOrStop等方法上加入CacheEvict注解

添加购物车

需求分析

image-20250712212630069

数据库设计

  • 作用:暂时存放所选商品的地方
  • 选的什么商品
  • 每个商品都买了几个
  • 不同用户的购物车需要区分开
字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 商品名称 冗余字段
image varchar(255) 商品图片路径 冗余字段
user_id bigint 用户id 逻辑外键
dish_id bigint 菜品id 逻辑外键
setmeal_id bigint 套餐id 逻辑外键
dish_flavor varchar(50) 菜品口味
number int 商品数量
amount decimal(10,2) 商品单价 冗余字段
create_time datetime 创建时间

查看购物车

需求分析

image-20250712223757849

清空购物车

需求分析

image-20250712224659704

地址簿

需求分析

接口设计:

  • 新增地址
  • 查询当前登录用户的所有地址信息
  • 查询默认地址
  • 根据id修改地址
  • 根据id删除地址
  • 根据id查询地址
  • 设置默认地址

用户下单

需求分析

image-20250713154150730

image-20250713160925447

image-20250713161124085

image-20250713161218601

数据库设计:订单表orders

字段名 数据类型 说明 备注
id bigint 主键 自增
number varchar(50) 订单号
status int 订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
user_id bigint 用户id 逻辑外键
address_book_id bigint 地址id 逻辑外键
order_time datetime 下单时间
checkout_time datetime 付款时间
pay_method int 支付方式 1微信支付 2支付宝支付
pay_status tinyint 支付状态 0未支付 1已支付 2退款
amount decimal(10,2) 订单金额
remark varchar(100) 备注信息
phone varchar(11) 手机号 冗余字段
address varchar(255) 详细地址信息 冗余字段
consignee varchar(32) 收货人
cancel_reason varchar(255) 订单取消原因
rejection_reason varchar(255) 拒单原因
cancel_time datetime 订单取消时间
estimated_delivery_time datetime 预计送达时间
delivery_status tinyint 配送状态 1立即送出 0选择具体时间
delivery_time datetime 送达时间
pack_amount int 打包费
tableware_number int 餐具数量
tableware_status tinyint 餐具数量状态 1按餐量提供 0选择具体数量

数据库设计:订单明细表order_detail

字段名 数据类型 说明 备注
id bigint 主键 自增
name varchar(32) 商品名称 冗余字段
image varchar(255) 商品图片路径 冗余字段
order_id bigint 订单id 逻辑外键
dish_id bigint 菜品id 逻辑外键
setmeal_id bigint 套餐id 逻辑外键
dish_flavor varchar(50) 菜品口味
number int 商品数量
amount decimal(10,2) 商品单价

image-20250713162430533

image-20250713162442209

订单支付

image-20250713201603628

image-20250713201756989

流程

image-20250713204911005

image-20250713205043917

image-20250713205314341

微信支付准备工作

调用过程如何保证数据安全?
微信后台如何调用到商户系统?

调用微信支付的API需要有公网ip

image-20250713215447131

Spring Task

介绍

定时任务

image-20250717165706000

应用场景:

  • 信用卡每月还款提醒
  • 银行贷款每月还款提醒
  • 火车票售票系统处理未支付订单
  • 入职纪念日为用户发送通知

只要是需要定时处理的场景都可以使用SpringTask

cron表达式

Cron表达式是一个字符串,用于定义任务触发的时间,由6或7个域组成(空格分隔),每个域代表特定时间含义。

1. 构成规则

含义 是否可选
第1域 必选
第2域 分钟 必选
第3域 小时 必选
第4域 必选
第5域 必选
第6域 必选
第7域 可选

2. 示例:2022年10月12日上午9点整

分钟 小时
0 0 9 12 10 ? 2022

对应的Cron表达式0 0 9 12 10 ? 2022

说明

  • 周域(第6域) 使用 ? 表示不指定(避免与“日”域冲突,两者通常只指定一个)。
  • 年域(第7域) 可省略,省略后表达式为6个域(如 0 0 9 12 10 ? 表示每年10月12日9点触发)。

image-20250717171429370

用ai和网站就行

入门案例

image-20250717200110733

订单状态定时处理

需求分析

用户下单后可能存在的异常状态

  • 待支付超时:下单后未支付,订单一直处于“待支付”状态。
  • 派送中未完成:用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态。

定时任务解决方案

通过定时任务自动修正异常订单状态,具体逻辑如下:

定时任务场景 触发频率 判定条件 处理逻辑
支付超时订单处理 每分钟检查一次 下单后超过15分钟仍未支付 订单状态改为“已取消”
派送中订单自动完成 每天凌晨1点检查 订单状态为“派送中”(默认用户已收货) 订单状态改为“已完成”

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package com.sky.task;


import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

/**
* 订单定时任务
*/
@Component
@Slf4j
public class OrderTask {

@Autowired
private OrderMapper orderMapper;

/**
* 处理支付超时订单
*/
@Scheduled(cron = "0 * * * * ? ") //每分钟触发一次
public void processTimeoutOrder(){
log.info("处理订单超时,{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
List<Orders> byStatusAndOrderTime = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT, time);
if (byStatusAndOrderTime != null && byStatusAndOrderTime.size() > 0){
for (Orders order : byStatusAndOrderTime) {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("订单超时取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
}
}
}

/**
* 处理派送中的订单
*/
@Scheduled(cron = "0 0 1 * * ? ") //每天凌晨1点触发一次
public void processDeliveryOrder(){
log.info("处理派送中的订单,{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> statusAndOrderTime = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS, time);
if (statusAndOrderTime != null && statusAndOrderTime.size() > 0){
for (Orders order : statusAndOrderTime) {
order.setStatus(Orders.CANCELLED);
orderMapper.update(order);
}
}
}

}

WebSocket(网络协议)

介绍

不需要刷新

image-20250717220553089

应用场景:

  • 视频弹幕
  • 网页聊天
  • 体育实况更新
  • 股票基金报价实时更新

来单提醒

需求分析

实现目标

通过WebSocket实现管理端页面与服务端的长连接,实时推送订单相关消息(如来单提醒、客户催单)。

核心设计

  1. 长连接保持
    • 通过WebSocket协议实现管理端页面与服务端的持久连接,避免HTTP短轮询的性能损耗。
  2. 消息推送触发时机
    • 当客户完成支付后,服务端调用WebSocket API主动向管理端推送消息。
  3. 客户端消息处理
    • 管理端浏览器解析服务端推送的JSON消息,根据消息类型触发消息提示语音播报(如来单提醒时自动播放语音)。

消息数据格式(JSON)

服务端推送的消息固定包含以下字段:

字段名 说明 取值示例
type 消息类型 1(来单提醒)、2(客户催单)
orderId 订单ID 1001(关联具体订单)
content 消息内容 "您有新的订单,请及时处理!"

客户催单

需求分析

核心设计

  • 长连接保持:通过WebSocket实现管理端页面和服务端保持长连接状态。

  • 消息触发时机:当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息。

  • 客户端处理:客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。

  • 数据格式约定

    :服务端发送给客户端浏览器的JSON数据格式包含以下字段:

    • type:消息类型(1为来单提醒,2为客户催单)
    • orderId:订单id
    • content:消息内容

image-20250718155236429

Apache ECharts(前端的技术)

介绍

image-20250718161640965

image-20250718161656373

image-20250718161708948

image-20250718161720221

image-20250718165310087

image-20250718165342504

营业额统计

需求分析

image-20250718165715085

image-20250718170859329

用户统计

需求分析

image-20250718214735655

image-20250718215022865

订单统计

需求分析

image-20250718223557825

image-20250718223700712

销量排名统计

需求分析

image-20250718232236512

image-20250718232608021

工作台

需求分析

image-20250719103659093

名词解释:

  • 营业额:已完成订单的总金额
  • 有效订单:已完成订单的数量
  • 订单完成率:有效订单数/总订单数*100%
  • 平均客单价:营业额/有效订单数
  • 新增用户:新增用户的数量

接口设计:

  • 今日数据接口
  • 订单管理接口
  • 菜品总览接口
  • 套餐总览接口
  • 订单搜索 (已完成)
  • 各个状态的订单数量统计 (已完成)

image-20250719104309427

image-20250719104408982

image-20250719104423227

image-20250719104452228

ApachePOI

## 介绍


image-20250719110545423

ApachePOI的应用场景:

  • 银行网银系统导出交易明细
  • 各种业务系统导出Excel报表
  • 批量导入业务数据

ApachePOl_入门案例_

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
package com.sky.test;

import org.apache.poi.xssf.usermodel.XSSFCell;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;

/**
* 使用POI操作Excel文件
*/
public class POITest {

/**
* 通过POI创建Excel文件并且写入文件内容
*/
public static void write() throws Exception{
//在内存中创建一个Excel文件
XSSFWorkbook excel = new XSSFWorkbook();
//在Excel文件中创建一个Sheet页
XSSFSheet sheet = excel.createSheet("info");
//在Sheet中创建行对象,rownum编号从0开始
XSSFRow row = sheet.createRow(1);
//创建单元格并且写入文件内容
row.createCell(1).setCellValue("姓名");
row.createCell(2).setCellValue("城市");

//创建一个新行
row = sheet.createRow(2);
row.createCell(1).setCellValue("张三");
row.createCell(2).setCellValue("北京");

row = sheet.createRow(3);
row.createCell(1).setCellValue("李四");
row.createCell(2).setCellValue("南京");

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

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


/**
* 通过POI读取Excel文件中的内容
* @throws Exception
*/
public static void read() throws Exception{
InputStream in = new FileInputStream(new File("D:\\info.xlsx"));

//读取磁盘上已经存在的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//读取Excel文件中的第一个Sheet页
XSSFSheet sheet = excel.getSheetAt(0);

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

for (int i = 1; i <= lastRowNum ; i++) {
//获得某一行
XSSFRow row = sheet.getRow(i);
//获得单元格对象
String cellValue1 = row.getCell(1).getStringCellValue();
String cellValue2 = row.getCell(2).getStringCellValue();
System.out.println(cellValue1 + " " + cellValue2);
}

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

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

导出运营数据Excel报表

需求分析和设计

image-20250719151151877

image-20250719151302564

先设计好一个模板文件

image-20250719151553252

代码开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 导出运营数据报表
*
* @param response
*/
@Override
public void exportBusinessData(HttpServletResponse response) {
// 1. 查询数据库,30天的营业数据
LocalDate beginDate = LocalDate.now().minusDays(30);
LocalDate endDate = LocalDate.now().plusDays(1);
LocalDateTime beginTime = LocalDateTime.of(beginDate, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(endDate, LocalTime.MAX);
BusinessDataVO businessData = workspaceService.getBusinessData(beginTime, endTime);
// 2.通过POI将数据导入到excel中

// 创建输入流
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
// 创建工作簿
try {
XSSFWorkbook excel = new XSSFWorkbook(in);
//填充数据
XSSFSheet sheet = excel.getSheet("Sheet1");
sheet.getRow(1).getCell(1).setCellValue(beginDate + "至" + endDate);
//获取第4行
XSSFRow row = sheet.getRow(3);
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(6).setCellValue(businessData.getNewUsers());

//获取第5行
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getUnitPrice());

//填充明细数据
for (int i=0;i<30;i++){
LocalDate date = beginDate.plusDays(i);
//查询填充的数据
BusinessDataVO businessData1 = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
//
row = sheet.getRow(7 + i);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessData1.getTurnover());
row.getCell(3).setCellValue(businessData1.getValidOrderCount());
row.getCell(4).setCellValue(businessData1.getOrderCompletionRate());
row.getCell(5).setCellValue(businessData1.getUnitPrice());
row.getCell(6).setCellValue(businessData1.getNewUsers());
}
// 3.通过输出流将excel下载到客户端
ServletOutputStream out = response.getOutputStream();
excel.write(out);
out.close();
excel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
其他文章
cover
Web笔记
  • 25/03/29
  • 17:48
  • 17.3k
  • 72
目录导航 置顶
  1. 1. 软件开发整体介绍
    1. 1.1. 软件开发流程
    2. 1.2. 角色分工
    3. 1.3. 软件环境
  2. 2. 项目
    1. 2.1. 项目介绍
    2. 2.2. 产品原型
    3. 2.3. 技术选型
  3. 3. 开发环境搭建
    1. 3.1. 前端环境
    2. 3.2. 后端环境
    3. 3.3. Git
    4. 3.4. 数据库环境
    5. 3.5. 前后端联调
      1. 3.5.1. 思考
      2. 3.5.2. Nginx
    6. 3.6. 完善登录功能(加密密码)
      1. 3.6.1. 完善登录功能
  4. 4. 导入接口文档
  5. 5. Swagger
    1. 5.1. 介绍
      1. 5.1.1. 1. Swagger
      2. 5.1.2. 2. Knife4j
      3. 5.1.3. 3. Maven 依赖(Knife4j 起步依赖)
    2. 5.2. 使用方式
      1. 5.2.1. 配置类示例(WebMvcConfiguration)
    3. 5.3. 思考
  6. 6. 新增员工
    1. 6.0.1. 需求分析和设计
    2. 6.0.2. 数据库设计(employee表)
    3. 6.0.3. 代码
    4. 6.0.4. 测试(视频多看)
    5. 6.0.5. 完善
  • 7. 员工分页查询
    1. 7.0.1. 需求分析和设计
    2. 7.0.2. 代码
    3. 7.0.3. 完善
  • 8. 启用禁用员工账号
    1. 8.0.1. 需求分析和设计
    2. 8.0.2. 代码
    3. 8.0.3. 完善
  • 9. 编辑员工
    1. 9.0.1. 需求分析和设计
    2. 9.0.2. 代码
  • 10. 导入分类模块功能代码
    1. 10.0.1. 需求分析和设计
  • 11. 公共字段自动填充
    1. 11.1. 问题分析
    2. 11.2. 实现思路
  • 12. 新增菜品
    1. 12.1. 需求分析和设计
    2. 12.2. 代码
  • 13. 菜品分页查询
    1. 13.1. 需求分析和设计
    2. 13.2. 代码
  • 14. 删除菜品
    1. 14.1. 需求分析和设计
    2. 14.2. 代码
  • 15. 修改菜品
    1. 15.0.1. 需求分析和设计
    2. 15.0.2. 代码
  • 16. 套餐管理
  • 17. Redis
    1. 17.1. 介绍
    2. 17.2. 下载
    3. 17.3. 启动与注册window服务
    4. 17.4. Redis数据类型
      1. 17.4.1. 5种常用的数据类型
    5. 17.5. 常用命令
      1. 17.5.1. 字符串操作命令
      2. 17.5.2. 哈希操作命令
      3. 17.5.3. 列表操作命令
      4. 17.5.4. 集合操作命令
      5. 17.5.5. 有序集合操作命令
      6. 17.5.6. 通用命令
  • 18. 在Java中使用Redis
    1. 18.1. Redis的Java客户端
  • 19. Spring Data Redis
    1. 19.1. 使用方式
    2. 19.2. 操作
  • 20. 店铺营业状态设置
    1. 20.0.1. 需求分析和设计
    2. 20.0.2. 代码
    3. 20.0.3. 完善
  • 21. HttpClient
    1. 21.1. 介绍
      1. 21.1.1. HttpClient 介绍
    2. 21.2. 入门案例
  • 22. 微信小程序
    1. 22.1. 介绍
    2. 22.2. 准备工作
    3. 22.3. 入门案例
    4. 22.4. 微信登陆
      1. 22.4.1. 需求分析
    5. 22.5. 商品浏览
      1. 22.5.1. 需求分析
    6. 22.6. 菜品缓存
      1. 22.6.1. 问题说明
      2. 22.6.2. 实现思路
    7. 22.7. 缓存套餐
  • 23. Spring Cache
    1. 23.0.1. 实现思路
  • 23.1. 添加购物车
    1. 23.1.1. 需求分析
    2. 23.1.2. 数据库设计
  • 23.2. 查看购物车
    1. 23.2.1. 需求分析
  • 23.3. 清空购物车
    1. 23.3.1. 需求分析
  • 23.4. 地址簿
    1. 23.4.1. 需求分析
  • 23.5. 用户下单
    1. 23.5.1. 需求分析
  • 23.6. 订单支付
    1. 23.6.1. 流程
    2. 23.6.2. 微信支付准备工作
  • 24. Spring Task
    1. 24.1. 介绍
    2. 24.2. cron表达式
    3. 24.3. 入门案例
  • 25. 订单状态定时处理
    1. 25.1. 需求分析
    2. 25.2. 代码
  • 26. WebSocket(网络协议)
    1. 26.1. 介绍
  • 27. 来单提醒
    1. 27.1. 需求分析
  • 28. 客户催单
    1. 28.1. 需求分析
  • 29. Apache ECharts(前端的技术)
    1. 29.1. 介绍
  • 30. 营业额统计
    1. 30.1. 需求分析
  • 31. 用户统计
    1. 31.1. 需求分析
  • 32. 订单统计
    1. 32.1. 需求分析
  • 33. 销量排名统计
    1. 33.1. 需求分析
  • 34. 工作台
    1. 34.1. 需求分析
  • 35. ApachePOI
    1. 35.1. ApachePOl_入门案例_
  • 36. 导出运营数据Excel报表
    1. 36.1. 需求分析和设计
    2. 36.2. 代码开发
  • 请输入关键词进行搜索