项目地址:congmucc/Sky_Takeaway: 外卖项目学习记录 (github.com) 已删除
学习项目记录
1 java
1.1 对于模块使用 && 结果类中的泛型的用法
1.1.1 idea中一个模块如何使用另一个模块的类
在 Maven 项目中,通过在
pom.xml
文件中添加依赖配置,就可以解决问题。但是非 Maven 项目呢?IDEA 很好地提供了一个解决方案:模块依赖
在 IDEA 中,通过 module(模块) 依赖来解决一个模块中使用另一个模块中的类_一个模块依赖另外一个模块,另外一个模块怎么用这个模块的接口_知音12138的博客-CSDN博客
1.1.2idea中结果类中的泛型的用法
- 示例代码
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询: {}", dishPageQueryDTO);
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult);
}
一般是在get请求中使用,对应的值就行
1.2 TODO使用
TODO: 英语翻译为待办事项,备忘录。如果代码中有该标识,说明在标识处有功能代码待编写,待实现的功能在说明中会简略说明
// TODO 后期需要进行md5加密,然后再进行比对
这里可以在侧栏TODO中直接跳转
1.3 DTO层&&VO层的使用以及方法
1.3.1 DTO层介绍
一般是一个实体类的一部分,用于其他功能的时候前端返回json数据进行封装,例如新增员工,有很多东西不需要,例如实体类中的修改时间,这些在新增员工的时候就不需要,但是穿过来的数据最好封装一下,就是DTO层。
1.3.2 对于实体类和DTO数据转换
- BeanUtils.copyProperties()
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
// 对象的属性拷贝 第一个为源,第二个为目标,源拷贝到目标
BeanUtils.copyProperties(employeeDTO, employee);
employeeMapper.save(employeeDTO);
}
DTO层在service层进入mapper层的时候最好转化为实体类,这是因为mapper层是面向数据库的。
1.3.3 VO层
这个层是处理逻辑的,也类似与DTO层,举个例子,我需要返回两张表中的数据,或者一些其他数据,最常见的就是分页查询,如果含有子查询,就可以设置一个VO
1.4 接口线程(获取线程内使用者的id)p20
前端每发送一次请求都是一个单独的线程,对于全局获取值,我们可以利用ThreadLocal来获取本线程的存储空间(一般会包装成一个工具类)
1.4.1 ThreadLocal
介绍:
ThreadLocal 并不是一个Thread,而是Thread的局部变量。 ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
常用方法:
- public void set(T value) 设置当前线程的线程局部变量的值
- public T get() 返回当前线程所对应的线程局部变量的值
- public void remove() 移除当前线程的线程局部变量
1.4.2 封装类
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();
}
}
这里是从jwt解码中获取当前登录的id,如果想要更多,可以将封装类设置成map,然后进行使用
1.5 消息转换器(日期时间转换)p24
前后端保存可以使用正常格式,这个格式就是LocalDateTime
例如:后端显示这个2023-09-20T00:54:26.841607400
数据库中显示为:2022-02-15 15:51:20
前端显示为20230920005426
前端不用修改
1.5.1 在属性上加上注解,对日期进行格式化
- 在实体类中
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
//@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
1.5.2 在WebMvcConfiguration中重写SpringMVC的消息转换器,统一对日期类型进行格式处理
/**
* 扩展Spring MVC框架的消息转化器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建一个消息转换器对象
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
converter.setObjectMapper(new JacksonObjectMapper());
//将自己的消息转化器加入容器中
converters.add(0,converter);
}
时间格式定义,sky-common模块中
package com.sky.json;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 对象映射器:基于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";
public JacksonObjectMapper() {
super();
//收到未知属性时不报异常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
最后前端格式:2023-09-20 00:58
1.5.3 url传参中携带多个日期如何接收 p141
背景:
这里是echarts表中前端通过url传过来起始时间和结束时间
步骤:
在形参中添加一个注解@DateTimeFormat(pattern = "yyyy-MM-dd")
代码:
/**
* 营业额统计
* @param begin 起始日期
* @param end 截至日期
* @return Result<TurnoverReportVO>
*/
@GetMapping("/turnoverStatistics")
@ApiOperation("营业额统计")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
return Result.success(reportService.getTurnoverStatistics(begin, end));
}
通过@DateTimeFormat这个注解设置格式接收后端传过来的数据
1.5.4 转化LocalDate和LocalDateTime p143
背景:
这里p141传过来的数据为LocalDate格式,但是数据库中是LocalDateTime ,所以说对比的话需要进行全部转化为LocalDateTime ,然后在mapper层进行比较
步骤:
使用LocalDateTime.of这个函数进行合并
代码:
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
LocalDateTime.of这个方法可以进行时间和日期的合并,可以搜一下
mix和min分别表示一天的最大和最小即23点59分59秒和0点0分0秒。源代码如下:
/** * The minimum supported {@code LocalTime}, '00:00'. * This is the time of midnight at the start of the day. */ public static final LocalTime MIN; /** * The maximum supported {@code LocalTime}, '23:59:59.999999999'. * This is the time just before midnight at the end of the day. */ public static final LocalTime MAX;
1.5.5 对于时间增加或者减少
背景:
对于某些需求我们需要比数据库中的时间大,或者小
步骤:
这里使用LocalDateTime和LocalDate自带的函数
代码
// 在当前时间上减去15分钟
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
// 在当前日期上增加1天
LocalDate date = LocalDate.now().plusDays(1);
1.6 建造者模式 && 链式调用 p26
场景:
如果想要new一个自定义值的实体类对象,可以使用建造者模式进行链式调用,更直观。
步骤:
需要使用在实体类中使用lombok @Builder注解
public void startOrStop(Integer status, Long id) {
// 这个是对于单个更新,我们可以使用一个动态sql进行复用,然后传给持久层时需要传一个实体对象更利于复用
// 普通情况下
// Employee employee = new Employee();
// employee.setStatus(status);
// employee.setId(id);
// 有builder的情况下,建造者模式,链式
Employee employee = Employee.builder()
.status(status)
.id(id)
.build();
employeeMapper.update(employee);
}
1.7 切面式编程--公共字段填充 p32
公共字段填充的意思是: 如果在更新或者插入的时候需要输入更新人的id,时间,插入时的id,时间,创建时间,创建人
可以利用切面思想进行自动填充。
1.7.1 自定义注解AutoFill, 用于标识需要进行公共字段自动填充的方法
- com.sky.annotation
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
*/
@Target(ElementType.METHOD) // 指定注解只能加在方法上面
@Retention(RetentionPolicy.RUNTIME) //表示该注解的信息将在运行时可见和可获取
public @interface AutoFill {
// 指定数据库操作类型 UPDATE, INSERT
OperationType value();
}
annotation这个包是专门存放自定义注解的
1.7.2 自定义切面类 统一拦截加入了AutoFill注解的方法,通过反射为公共字段赋值
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
/**
* 自定义切面类, 实现公共字段填充处理逻辑
*/
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
/**
* 切入点
*/
// "execution(* com.sky.mapper.*.*(..)) 第一个 '*' 为返回值所有,
// 'mapper.*.*' 这个意思是mapper中所有的类(mapper后的第一个 ‘*’),所有的方法(第二个)
// @annotation(com.sky.annotation.AutoFill)" 这个是要拦截的那些注解
@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut(){}
/**
* 前置通知 @Before("切入点函数")
*/
@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(); // 获得数据库的操作类型 EmployeeMapper中的autofill中的value
// 获取当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs(); // 获得所有的实体类方法 EmployeeMapper中的方法
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 {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
// 通过反射来为对象属性赋值
setCreateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
throw new RuntimeException(e);
}
} 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) {
throw new RuntimeException(e);
}
}
}
}
1.7.3 在Mapper的方法上加入AutoFill注解
@AutoFill(value = OperationType.INSERT)
void insert(Category category);
直接使用即可
1.8 使用阿里云Oss存储图片 p37
1.8.1进行yml中配置相应的文件
- sky-server/src/main/resources/application.yml
spring:
profiles:
active: dev
main:
allow-circular-references: true
sky:
alioss:
endpoint: ${sky.alioss.endpoint}
assess-key-id: ${sky.alioss.assess-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
这里可以将key写入到环境变量中,这样可以直接避免上传到github上的时候被攻击
- sky-server/src/main/resources/application-dev.yml
sky:
alioss:
endpoint: oss-cn-beijing.aliyuncs.com
access-key-id: LTAI5tPBUNpiqnX1aE3oMxrz
access-key-secret: RjBtzgZGxggKthpFmauys1qU3u1Fts
bucket-name: congmu-sky-takeaway
这个是application.yml从application-dev.yml中引入的,dev是生产环境的yml,到时候可以快速修改
1.8.2 编写实体类,获取yml文件中的数据
- com.sky.properties.AliOssProperties
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;
}
这个直接@ConfigurationProperties(prefix = "sky.alioss")使用这个来进行一键绑定了,并且这种类都在properties包下
1.8.3 编写工具类,将文件上传工具类
package com.sky.utils;
import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.io.ByteArrayInputStream;
@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
/**
* 文件上传
*
* @param bytes
* @param objectName
* @return
*/
public String upload(byte[] bytes, String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
// 创建PutObject请求。
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
//文件访问路径规则 https://BucketName.Endpoint/ObjectName
StringBuilder stringBuilder = new StringBuilder("https://");
stringBuilder
.append(bucketName)
.append(".")
.append(endpoint)
.append("/")
.append(objectName);
log.info("文件上传到:{}", stringBuilder.toString());
return stringBuilder.toString();
}
}
该是阿里云官方的代码,做了些许修改,例如适配1.8.1和2
1.8.4 编写配置类, 直接在项目启动的时候将1.8.2的类的数据赋值给1.8.3工具类
package com.sky.config;
import com.sky.properties.AliOssProperties;
import com.sky.utils.AliOssUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 配置类 用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {
@Bean
// 保证spring容器中只有一个对象
@ConditionalOnMissingBean
public AliOssUtil aliOssUtil(AliOssProperties aliOssProperties) {
log.info("开始创建阿里云文件上传工具类对象:{}", aliOssProperties);
return new AliOssUtil(aliOssProperties.getEndpoint(),
aliOssProperties.getAccessKeyId(),
aliOssProperties.getAccessKeySecret(),
aliOssProperties.getBucketName());
}
}
1.8.5 controller 层使用
package com.sky.controller.admin;
import com.sky.constant.MessageConstant;
import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
@Slf4j
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
/**
* 文件上传
* @param file
* @return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{} ", file);
try {
// 原始文件名
String originalFilename = file.getOriginalFilename();
// 截取原始文件名的后缀
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
// 构建新文件名称
String objectName = UUID.randomUUID().toString() + extension;
// 文件的请求路径
String filePath = aliOssUtil.upload(file.getBytes(), objectName);
return Result.success(filePath);
} catch (IOException e) {
log.error("文件上传失败: {}", e);
// throw new RuntimeException(e);
}
return Result.error(MessageConstant.UPLOAD_FAILED);
}
}
中间使用uuid进行重命名即可
1.9 Transactional注解使用 && sql插入后获取id p38-39
当涉及到多张表的时候保证方法是一个原子性的, 例如本例中,新增菜品和口味,全成功或者全失败
记得在启动类上开启注解方式的事务管理
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
- xml中
<insert id="insert" useGeneratedKeys="true" keyProperty="id">
这个useGeneratedKeys是获取插入后生成的id,keyProperty是将获取后的id注入实体类
- ServiceImpl中
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
// 像菜品表中插入1条数据
dishMapper.insert(dish);
// 获取insert语句生成的id
Long dishId = dish.getId();
// 向口味表中插入n条数据
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
// 将获取的dishId插入到口味表中
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(dishId);
});
DishFlavorMapper.insertBatch(flavors);
}
获取实体类id就可以使用了, 可以将dishId插入到口味表中
1.10 Spring Cache p91
1.10.1 介绍
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>
常用注解:
在SpringCache中提供了很多缓存操作的注解,常见的是以下的几个:
注解 | 说明 |
---|---|
@EnableCaching | 开启缓存注解功能,通常加在启动类上 |
@Cacheable | 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中 |
@CachePut | 将方法的返回值放到缓存中 |
@CacheEvict | 将一条或多条数据从缓存中删除 |
在spring boot项目中,使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。
例如,使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。
1.10.2 使用
这个跟redis联合使用, 只不过将redis改为注解的方式来写了
[3.4 通过redis来将数据存到内存中 p87](#3.4 通过redis来将数据存到内存中 p87)
- @Cacheable例子(第九行)
/**
* 条件查询
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询套餐")
@Cacheable(cacheNames = "setmealCache",key = "#categoryId") //key: setmealCache::100
public Result<List<Setmeal>> list(Long categoryId) {
Setmeal setmeal = new Setmeal();
setmeal.setCategoryId(categoryId);
setmeal.setStatus(StatusConstant.ENABLE);
List<Setmeal> list = setmealService.list(setmeal);
return Result.success(list);
}
- @CacheEvict
@CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
public Result save(@RequestBody SetmealDTO setmealDTO) {
setmealService.saveWithDish(setmealDTO);
return Result.success();
}
@CacheEvict(cacheNames = "setmealCache",allEntries = true) // allEntries = true这个是删除该setmealCache下的全部
1.11 微信支付
这里需要了解相关步骤,暂时掠过
1.12 Spring Task p124
步骤:
- 导入maven坐标 spring-context(已存在)
- 启动类添加注解 @EnableScheduling 开启任务调度
- 自定义定时任务类
导入maven坐标 spring-context(已存在)
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency>
启动类之中已经存在
启动类添加注解 @EnableScheduling 开启任务调度
如题即可
自定义定时任务类
com.sky.task.OrderTask
javapackage 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 = "* 1 * * * ? ") // 每分钟触发一次 public void processTimeoutOrder() { log.info("定时处理超时订单: {}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-15); List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time); if (ordersList != null && ordersList.size() > 0) { for (Orders orders : ordersList) { orders.setStatus(Orders.CANCELLED); orders.setCancelReason("订单超时,自动取消"); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); } } } /** * 处理一直处于派送中状态的订单 */ @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点 public void processDeliveryOrder() { log.info("定时处理处于派送中的订单: {}", LocalDateTime.now()); LocalDateTime time = LocalDateTime.now().plusMinutes(-60); List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time); log.info("处于派送中的订单: {}", ordersList); if (ordersList != null && ordersList.size() > 0) { for (Orders orders : ordersList) { orders.setStatus(Orders.COMPLETED); orderMapper.update(orders); } } } }
1.13 WebSocket p129
背景:
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
使用场景:
视频弹幕
网页聊天
体育实况更新
股票基金报价实时更新
步骤:
- WebSocket小案例
- 来单提醒
- 客户催单
1.13.1 WebSocket小案例
步骤:
直接使用websocket.html页面作为WebSocket客户端
导入WebSocket的maven坐标
导入WebSocket服务端组件WebSocketServer,用于和客户端通信
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
导入定时任务类WebSocketTask,定时向客户端推送数据
直接使用websocket.html页面作为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"> var websocket = null; var clientId = Math.random().toString(36).substr(2); //判断当前浏览器是否支持WebSocket if('WebSocket' in window){ //连接WebSocket节点 websocket = new WebSocket("ws://localhost:8080/ws/"+clientId); } else{ alert('Not support websocket') } //连接发生错误的回调方法 websocket.onerror = function(){ setMessageInnerHTML("error"); }; //连接成功建立的回调方法 websocket.onopen = function(){ setMessageInnerHTML("连接成功"); } //接收到消息的回调方法 websocket.onmessage = function(event){ setMessageInnerHTML(event.data); } //连接关闭的回调方法 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>
导入WebSocket的maven依赖
java<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>
导入WebSocket服务端组件WebSocketServer,用于和客户端通信
javapackage com.sky.websocket; import org.springframework.stereotype.Component; import javax.websocket.OnClose; import javax.websocket.OnMessage; import javax.websocket.OnOpen; import javax.websocket.Session; import javax.websocket.server.PathParam; import javax.websocket.server.ServerEndpoint; import java.util.Collection; import java.util.HashMap; import java.util.Map; /** * WebSocket服务 */ @Component @ServerEndpoint("/ws/{sid}") public class WebSocketServer { //存放会话对象 private static Map<String, Session> sessionMap = new HashMap(); /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { System.out.println("客户端:" + sid + "建立连接"); sessionMap.put(sid, session); } /** * 收到客户端消息后调用的方法 * * @param message 客户端发送过来的消息 */ @OnMessage public void onMessage(String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } /** * 连接关闭调用的方法 * * @param sid */ @OnClose public void onClose(@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } /** * 群发 * * @param message */ public void sendToAllClient(String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { //服务器向客户端发送消息 session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
这里群发是服务端向客户端发送消息,最重要的
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
javapackage com.sky.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; /** * WebSocket配置类,用于注册WebSocket的Bean */ @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }
导入定时任务类WebSocketTask,定时向客户端推送数据
javapackage com.sky.task; import com.sky.websocket.WebSocketServer; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @Component public class WebSocketTask { @Autowired private WebSocketServer webSocketServer; /** * 通过WebSocket每隔5秒向客户端发送消息 */ @Scheduled(cron = "0/5 * * * * ?") public void sendMessageToClient() { webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now())); } }
1.13.2 来单提醒
背景:
有订单的情况下会弹出提示框提示商家有新订单并播报语音
- 弹出提示框
- 语音播报
设计思路:
- 通过WebSocket实现管理端页面和服务端保持长连接状态
- 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
- 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
- type 为消息类型,1为来单提醒 2为客户催单
- orderId 为订单id
- content 为消息内容
代码开发:
在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:
@Autowired
private WebSocketServer webSocketServer;
/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
//////////////////////////////////////////////
Map map = new HashMap();
map.put("type", 1);//消息类型,1表示来单提醒
map.put("orderId", orders.getId());
map.put("content", "订单号:" + outTradeNo);
//通过WebSocket实现来单提醒,向客户端浏览器推送消息
webSocketServer.sendToAllClient(JSON.toJSONString(map));
///////////////////////////////////////////////////
}
1.13.3 用户催单
背景:
用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:
- 语音播报
- 弹出提示框
设计思路:
- 通过WebSocket实现管理端页面和服务端保持长连接状态
- 当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
- type 为消息类型,1为来单提醒 2为客户催单
- orderId 为订单id
- content 为消息内容
代码实现:
/**
* 用户催单
*
* @param id
*/
@Override
public void reminder(Long id) {
// 查询订单是否存在
Orders orders = orderMapper.getById(id);
if (orders == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
//基于WebSocket实现催单
Map map = new HashMap();
map.put("type", 2);//2代表用户催单
map.put("orderId", id);
map.put("content", "订单号:" + orders.getNumber());
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}
1.14 一些工具类使用
1.14.1 StringUtils
导入依赖
java<dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency>
将map对象转化为string并用逗号相隔开(StringUtils.join())
List<LocalDate> dateList = new ArrayList<>(); dateList.add(begin); while (!begin.equals(end)) { // 日期计算,计算指定日期的后一天对应日期 begin = begin.plusDays(1); dateList.add(begin); } // 将list集合中每个元素转化成以逗号分割的字符串 String string = StringUtils.join(dateList, ',');
1.14.2 BeanUtils
导入依赖
java<!-- BeanUtils的依赖 --> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.9.4</version> </dependency>
- 对于实体类和DTO数据转换(BeanUtils.copyProperties())
[1.3.2 对于实体类和DTO数据转换](# 1.3.2 对于实体类和DTO数据转换)
1.15 sql书写
1.15.1 含有时间的map
背景:
实参传过来的是一个map集合,同时map中含时间,需要使用转义字符
步骤:
- 封装map集合
- 如果实参传过来的是一个map该如何使用
- 转义字符
封装map集合
javaLocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); Map map = new HashMap(); map.put("status", Orders.COMPLETED); map.put("begin",beginTime); map.put("end", endTime); Double turnover = orderMapper.sumByMap(map);
注意,键之后需要使用,键值的类型为LocalDateTime
如果实参传过来的是一个map该如何使用
sql<select id="sumByMap" resultType="java.lang.Double"> select sum(amount) from orders <where> <if test="status != null"> and status = #{status} </if> <if test="begin != null"> and order_time >= #{begin} </if> <if test="end != null"> and order_time <= #{end} </if> </where> </select>
这里面的值为上面的键
转义字符
sqland order_time >= #{begin} and order_time <= #{end}
这两句中的大于号和小于号是转义字符,因为是开始和结束为时间,为了防止被xml识别为标签的开始和结束符号,所以说要使用转义字符
<
< >
1.16 Apache POI
Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下,POI 都是用于操作 Excel 文件。
1.16.1 Apache POI入门案例
- 导入Maven依赖
- 代码开发,写入,读取
导入Maven依赖
java<dependency> <groupId>org.apache.poi</groupId> <artifactId>poi</artifactId> <version>3.16</version> </dependency> <dependency> <groupId>org.apache.poi</groupId> <artifactId>poi-ooxml</artifactId> <version>3.16</version> </dependency>
代码开发,写入,读取
javapackage com.sky.test; 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; /** * 使用PIT操作Excel文件 */ public class POITest { /** * 通过POI创建Excel文件并写入文件内容 * @throws Exception */ 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 outputStream = new FileOutputStream((new File("D:\\info.xlsx"))); excel.write(outputStream); // 关闭资源 outputStream.close(); excel.close(); } /** * 通过POI读取Excel文件中的内容 * @throws Exception */ public static void read() throws Exception { // 磁盘上读取文件 FileInputStream fileInputStream = new FileInputStream("D:\\info.xlsx"); // 读取磁盘上已经存在的Excel文件 XSSFWorkbook excel = new XSSFWorkbook(fileInputStream); // 读取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); } // 关闭资源 fileInputStream.close(); excel.close(); } public static void main(String[] args) throws Exception { // write(); read(); } }
1.16.2 项目使用
背景:
数据统计导出,导出报表文件,导出近30天的运营记录
步骤:
设计Excel模板文件
查询近30天的运营数据
将查询到的运营数据写入模板文件
通过输出流将Excel文件下载到客户端浏览器
设计Excel模板文件
这个是自己设计excel空模板,然后放入到
src/main/resources/template/运营数据报表模板.xlsx
这个路径中
查询近30天的运营数据
2.1 Controller层
根据接口定义,在ReportController中创建export方法:
java/** * 导出运营数据报表 * @param response */ @GetMapping("/export") @ApiOperation("导出运营数据报表") public void export(HttpServletResponse response){ reportService.exportBusinessData(response); }
2.2 Service层接口
在ReportService接口中声明导出运营数据报表的方法:
java/** * 导出近30天的运营数据报表 * @param response **/ void exportBusinessData(HttpServletResponse response);
2.3 Service层实现类
在ReportServiceImpl实现类中实现导出运营数据报表的方法:
提前将资料中的运营数据报表模板.xlsx拷贝到项目的resources/template目录中
java/**导出近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)); InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx"); try { //基于提供好的模板文件创建一个新的Excel表格对象 XSSFWorkbook excel = new XSSFWorkbook(inputStream); //获得Excel文件中的一个Sheet页 XSSFSheet sheet = excel.getSheet("Sheet1"); sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end); //获得第4行 XSSFRow row = sheet.getRow(3); //获取单元格 row.getCell(2).setCellValue(businessData.getTurnover()); row.getCell(4).setCellValue(businessData.getOrderCompletionRate()); row.getCell(6).setCellValue(businessData.getNewUsers()); 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 = begin.plusDays(i); //准备明细数据 businessData = 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(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(); } }
将查询到的运营数据写入模板文件
通过输出流将Excel文件下载到客户端浏览器
2 nginx
2.1 nginx反向代理
nginx 反向代理,就是将前端发送的动态请求由 nginx 转发到后端服务器
前端请求地址:http://localhost/api/employee/login
后端接口地址:http://localhost:8080/admin/employee/login
两者不一,可以使用nginx反向代理进行转发
2.1.1 nginx 反向代理的配置方式:
server{
listen 80;
server_name localhost;
location /api/{
proxy_pass http://localhost:8080/admin/; #反向代理
}
}
**proxy_pass:**该指令是用来设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。
如上代码的含义是:监听80端口号, 然后当我们访问 http://localhost:80/api/../..这样的接口的时候,它会通过 location /api/ {} 这样的反向代理到 http://localhost:8080/admin/上来。
3 Redis p50
3.1 启动Redis
redis-server.exe redis.windows.conf
3.2 Redis常用命令 p52
3.2.1 字符串
字符串(string): 普通字符串,Redis中最简单的数据类型
Redis 中字符串类型常用命令:
SET key value 设置指定key的值
GET key 获取指定key的值
SETEX key seconds value 设置指定key的值,并将 key 的过期时间设为 seconds 秒
SETNX key value 只有在 key不存在时设置 key 的值
3.2.2 哈希
哈希(hash):也叫散列,类似于Java中的HashMap结构
Redis hash 是一个string类型的 field 和 value 的映射表,hash特别适合用于存储对象,常用命令:
HSET key field value 将哈希表 key 中的字段 field 的值设为 value
HGET key field 获取存储在哈希表中指定字段的值
HDEL key field 删除存储在哈希表中的指定字段
HKEYS key 获取哈希表中所有字段
HVALS key 获取哈希表中所有值
3.2.3 列表
列表(list): 按照插入顺序排序,可以有重复元素,类似于Java中的LinkedList
Redis 列表是简单的字符串列表,按照插入顺序排序,常用命令:
LPUSH key value1 [value2] 将一个或多个值插入到列表头部
LRANGE key start stop 获取列表指定范围内的元素
RPOP key 移除并获取列表最后一个元素
LLEN key 获取列表长度
BRPOP key1 [key2 ] timeout 移出并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止
3.2.4 集合
集合(set):无序集合,没有重复元素,类似于Java中的HashSet
Redis set 是string类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据,常用命令:
SADD key member1 [member2] 向集合添加一个或多个成员
SMEMBERS key 返回集合中的所有成员
SCARD key 获取集合的成员数
SINTER key1 [key2] 返回给定所有集合的交集
SUNION key1 [key2] 返回所有给定集合的并集
SREM key member1 [member2] 移除集合中一个或多个成员
3.2.5 有序集合
有序集合(sorted set /zset): 集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素
Redis有序集合是string类型元素的集合,且不允许有重复成员。每个元素都会关联一个double类型的分数。常用命令:
ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
ZINCRBY key increment member 有序集合中对指定成员的分数加上增量 increment
ZREM key member [member ...] 移除有序集合中的一个或多个成员
3.2.6 通用命令
Redis的通用命令是不分数据类型的,都可以使用的命令:
KEYS pattern 查找所有符合给定模式( pattern)的 key
EXISTS key 检查给定 key 是否存在
TYPE key 返回 key 所储存的值的类型
DEL key 该命令用于在 key 存在是删除 key
3.3 SpringBoot Data Redis使用 p58
3.3.1 导入Spring Data Redis 的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.3.2 配置Redis数据源
在application-dev.yml中添加
sky:
redis:
host: localhost
port: 6379
password: 123456
database: 10
解释说明:
database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。
可以通过修改Redis配置文件来指定数据库的数量。
在application.yml中添加读取application-dev.yml中的相关Redis配置
spring:
profiles:
active: dev
redis:
host: ${sky.redis.host}
port: ${sky.redis.port}
password: ${sky.redis.password}
database: ${sky.redis.database}
3.3.3 编写配置类,创建RedisTemplate对象
package com.sky.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@Configuration
@Slf4j
public class RedisConfiguration {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate redisTemplate = new RedisTemplate();
// 设置redis的连接工厂对象
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置redis key的序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
return redisTemplate;
}
}
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象,但是默认的key序列化器为
JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为StringRedisSerializer序列化器。
3.3.4 java通过RedisTemplate对象操作Redis
- 创建测试类
package com.sky.test;
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);
//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();
}
/**
* 操作字符串
*/
@Test
public void testString() {
// set get setex setnx
redisTemplate.opsForValue().set("city", "北京");
String city = (String) redisTemplate.opsForValue().get("city");
System.out.println("String中的city:" + city);
redisTemplate.opsForValue().set("code", "1234", 3, TimeUnit.MINUTES);
redisTemplate.opsForValue().setIfAbsent("lock", "1");
redisTemplate.opsForValue().setIfAbsent("lock", "2");
}
/**
* 操作哈希类型的数据
*/
@Test
public void testHash() {
//hset hget hdel hkeys hvals
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("100", "name", "tom");
hashOperations.put("100", "age", "20");
String name = (String)hashOperations.get("100", "name");
System.out.println("hash中的name:" + name);
Set keys = hashOperations.keys("100");
System.out.println("hash中获取所有的keys: " + keys);
List values = hashOperations.values("100");
System.out.println("hash中获取所有的value:" + values);
}
/**
* 操作列表类型的数据
*/
@Test
public void testList(){
//lpush lrange rpop llen
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);
listOperations.rightPop("mylist");
Long size = listOperations.size("mylist");
System.out.println(size);
}
/**
* 操作集合类型的数据
*/
@Test
public void testSet(){
//sadd smembers 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 testZset(){
//zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1","a",10);
zSetOperations.add("zset1","b",12);
zSetOperations.add("zset1","c",9);
Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);
zSetOperations.incrementScore("zset1","c",10);
zSetOperations.remove("zset1","a","b");
}
/**
* 通用命令操作
*/
@Test
public void testCommon(){
//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");
}
}
3.4 通过redis来将数据存到内存中 p87
如果使用注解转到
[1.10.2 使用](#1.10.2 使用)
3.4.1设置redis
实际就是查的时候进行判断
- // 查询redis中是否存在菜品数据
- // 如果存在, 直接返回, 无需查询数据库
- // 如果不存在, 查询数据库,并将此查询放在redis中
就这个三步,如果是普通的话只有查询数据库
/**
* 根据分类id查询菜品
*
* @param categoryId
* @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
// 查询redis中是否存在菜品数据
String key = "dish_" + categoryId;
List<DishVO> redisList = (List<DishVO>) redisTemplate.opsForValue().get(key);
if (redisList != null && redisList.size() > 0) {
// 如果存在, 直接返回, 无需查询数据库
return Result.success(redisList);
}
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
// 如果不存在, 查询数据库,并将此查询放在redis中
List<DishVO> list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key, list);
return Result.success(list);
}
}
3.4.2 清除redis缓存数据
如果是修改了数据库中的内容,例如菜品价格,这时候需要清除redis中的数据,防止与数据库中的不一致, 以下是例子
为了保证数据库和Redis中的数据保持一致,修改管理端接口 DishController 的相关方法,加入清理缓存逻辑。
需要改造的方法:
- 新增菜品
- 修改菜品
- 批量删除菜品
- 起售、停售菜品
抽取清理缓存的方法:
在管理端DishController中添加
@Autowired
private RedisTemplate redisTemplate;
/**
* 清理缓存数据
* @param pattern
*/
private void cleanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
调用清理缓存的方法,保证数据一致性:
1). 新增菜品优化
/**
* 新增菜品
*
* @param dishDTO
* @return
*/
@PostMapping
@ApiOperation("新增菜品")
public Result save(@RequestBody DishDTO dishDTO) {
log.info("新增菜品:{}", dishDTO);
dishService.saveWithFlavor(dishDTO);
//清理缓存数据
String key = "dish_" + dishDTO.getCategoryId();
cleanCache(key);
return Result.success();
}
2). 菜品批量删除优化
/**
* 菜品批量删除
*
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品批量删除")
public Result delete(@RequestParam List<Long> ids) {
log.info("菜品批量删除:{}", ids);
dishService.deleteBatch(ids);
//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");
return Result.success();
}
3). 修改菜品优化
/**
* 修改菜品
*
* @param dishDTO
* @return
*/
@PutMapping
@ApiOperation("修改菜品")
public Result update(@RequestBody DishDTO dishDTO) {
log.info("修改菜品:{}", dishDTO);
dishService.updateWithFlavor(dishDTO);
//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");
return Result.success();
}
4). 菜品起售停售优化
/**
* 菜品起售停售
*
* @param status
* @param id
* @return
*/
@PostMapping("/status/{status}")
@ApiOperation("菜品起售停售")
public Result<String> startOrStop(@PathVariable Integer status, Long id) {
dishService.startOrStop(status, id);
//将所有的菜品缓存数据清理掉,所有以dish_开头的key
cleanCache("dish_*");
return Result.success();
}
4 vue3语法
4.1 ref使用
Vue3中操作ref的四种使用方式,建议收藏! - 掘金 (juejin.cn)
5 前端
5.1 Echart
Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。 官网地址:https://echarts.apache.org/zh/index.html
5.1.1 入门案例
Apache Echarts官方提供的快速入门:https://echarts.apache.org/handbook/zh/get-started/
实现步骤:
1). 引入echarts.js 文件(当天资料已提供)
2). 为 ECharts 准备一个设置宽高的 DOM
3). 初始化echarts实例
4). 指定图表的配置项和数据
5). 使用指定的配置项和数据显示图表
代码开发:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>ECharts</title>
<!-- 引入刚刚下载的 ECharts 文件 -->
<script src="echarts.js"></script>
</head>
<body>
<!-- 为 ECharts 准备一个定义了宽高的 DOM -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的dom,初始化echarts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
var option = {
title: {
text: 'ECharts 入门示例'
},
tooltip: {},
legend: {
data: ['销量']
},
xAxis: {
data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']
},
yAxis: {},
series: [
{
name: '销量',
type: 'bar',
data: [5, 20, 36, 10, 10, 20]
}
]
};
// 使用刚指定的配置项和数据显示图表。
myChart.setOption(option);
</script>
</body>
</html>
使用浏览器方式打开即可。
**总结:**使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。
5.1.2 Vue-Echarts
这是vue对Echarts的一个封装
官网地址:vue-echarts | Apache ECharts component for Vue.js. (ecomfe.github.io)