Skip to content

项目地址: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()
java
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 封装类 ​

java
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 在属性上加上注解,对日期进行格式化 ​

  • 在实体类中
java
//@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模块中

java
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")

代码:

java
/**
 * 营业额统计
 * @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自带的函数

代码

java
// 在当前时间上减去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(常用)

起步依赖:

xml
<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 ​

步骤:

  1. 导入maven坐标 spring-context(已存在)
  2. 启动类添加注解 @EnableScheduling 开启任务调度
  3. 自定义定时任务类
  1. 导入maven坐标 spring-context(已存在)

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

    启动类之中已经存在

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

    如题即可

  3. 自定义定时任务类

    com.sky.task.OrderTask

    java
    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 = "* 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 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。

使用场景:

  1. 视频弹幕

  2. 网页聊天

  3. 体育实况更新

  4. 股票基金报价实时更新

步骤:

  1. WebSocket小案例
  2. 来单提醒
  3. 客户催单

1.13.1 WebSocket小案例 ​

步骤:

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

  2. 导入WebSocket的maven坐标

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

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

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

  1. 直接使用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>
  2. 导入WebSocket的maven依赖

    java
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-websocket</artifactId>
            </dependency>
  3. 导入WebSocket服务端组件WebSocketServer,用于和客户端通信

    java
    package 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();
                }
            }
        }
    
    }

    这里群发是服务端向客户端发送消息,最重要的

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

    java
    package 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();
        }
    
    }
  5. 导入定时任务类WebSocketTask,定时向客户端推送数据

    java
    package 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 来单提醒 ​

背景:

​ 有订单的情况下会弹出提示框提示商家有新订单并播报语音

  1. 弹出提示框
  2. 语音播报

设计思路:

  • 通过WebSocket实现管理端页面和服务端保持长连接状态
  • 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
    • type 为消息类型,1为来单提醒 2为客户催单
    • orderId 为订单id
    • content 为消息内容

代码开发:

在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:

java
	@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>
  1. 将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>
  1. 对于实体类和DTO数据转换(BeanUtils.copyProperties())

[1.3.2 对于实体类和DTO数据转换](# 1.3.2 对于实体类和DTO数据转换)

1.15 sql书写 ​

1.15.1 含有时间的map ​

背景:

​ 实参传过来的是一个map集合,同时map中含时间,需要使用转义字符

步骤:

  1. 封装map集合
  2. 如果实参传过来的是一个map该如何使用
  3. 转义字符
  1. 封装map集合

    java
                LocalDateTime 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

  2. 如果实参传过来的是一个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 &gt;= #{begin}
            </if>
            <if test="end != null">
                and order_time &lt;= #{end}
            </if>
    
        </where>
    </select>

    这里面的值为上面的键

  3. 转义字符

    sql
    and order_time &gt;= #{begin}
    and order_time &lt;= #{end}

    这两句中的大于号和小于号是转义字符,因为是开始和结束为时间,为了防止被xml识别为标签的开始和结束符号,所以说要使用转义字符&lt; <  >

1.16 Apache POI ​

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。 一般情况下,POI 都是用于操作 Excel 文件。

1.16.1 Apache POI入门案例 ​

  1. 导入Maven依赖
  2. 代码开发,写入,读取
  1. 导入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>
  2. 代码开发,写入,读取

    java
    package 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天的运营记录

步骤:

  1. 设计Excel模板文件

  2. 查询近30天的运营数据

  3. 将查询到的运营数据写入模板文件

  4. 通过输出流将Excel文件下载到客户端浏览器

  1. 设计Excel模板文件

    这个是自己设计excel空模板,然后放入到src/main/resources/template/运营数据报表模板.xlsx

    这个路径中

  2. 查询近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();
            }
        }
  3. 将查询到的运营数据写入模板文件

  4. 通过输出流将Excel文件下载到客户端浏览器

2 nginx ​

2.1 nginx反向代理 ​

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

2.1.1 nginx 反向代理的配置方式: ​

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坐标 ​

xml
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.3.2 配置Redis数据源 ​

在application-dev.yml中添加

yaml
sky:
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 10

解释说明:

database:指定使用Redis的哪个数据库,Redis服务启动后默认有16个数据库,编号分别是从0到15。

可以通过修改Redis配置文件来指定数据库的数量。

在application.yml中添加读取application-dev.yml中的相关Redis配置

yaml
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 ​

实际就是查的时候进行判断

  1. // 查询redis中是否存在菜品数据
  2. // 如果存在, 直接返回, 无需查询数据库
  3. // 如果不存在, 查询数据库,并将此查询放在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中添加

java
	@Autowired
    private RedisTemplate redisTemplate;
	/**
     * 清理缓存数据
     * @param pattern
     */
    private void cleanCache(String pattern){
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }

调用清理缓存的方法,保证数据一致性:

1). 新增菜品优化

java
	/**
     * 新增菜品
     *
     * @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). 菜品批量删除优化

java
	/**
     * 菜品批量删除
     *
     * @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). 修改菜品优化

java
	/**
     * 修改菜品
     *
     * @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). 菜品起售停售优化

java
	/**
     * 菜品起售停售
     *
     * @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). 使用指定的配置项和数据显示图表

代码开发:

html
<!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)