项目地址: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.conf3.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 存在是删除 key3.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)