苍穹外卖小结上

发布时间 2023-11-13 20:41:24作者: 今晚三分饱

项目介绍

1.1项目介绍

本项目(苍穹外卖)是专门为餐饮企业(餐厅、饭店)定制的一款软件产品,包括 系统管理后台 和 小程序端应用 两部分。其中系统管理后台主要提供给餐饮企业内部员工使用,可以对餐厅的分类、菜品、套餐、订单、员工等进行管理维护,对餐厅的各类数据进行统计,同时也可进行来单语音播报功能。小程序端主要提供给消费者使用,可以在线浏览菜品、添加购物车、下单、支付、催单等。

1.2功能模块介绍

管理端用户端的具体业务功能模块如下:

1). 管理端功能

员工登录/退出 , 员工信息管理 , 分类管理 , 菜品管理 , 套餐管理 , 菜品口味管理 , 订单管理 ,数据统计,来单提醒。

模块 描述
登录/退出 内部员工必须登录后,才可以访问系统管理后台
员工管理 管理员可以在系统后台对员工信息进行管理,包含查询、新增、编辑、禁用等功能
分类管理 主要对当前餐厅经营的 菜品分类 或 套餐分类 进行管理维护, 包含查询、新增、修改、删除等功能
菜品管理 主要维护各个分类下的菜品信息,包含查询、新增、修改、删除、启售、停售等功能
套餐管理 主要维护当前餐厅中的套餐信息,包含查询、新增、修改、删除、启售、停售等功能
订单管理 主要维护用户在移动端下的订单信息,包含查询、取消、派送、完成,以及订单报表下载等功能
数据统计 主要完成对餐厅的各类数据统计,如营业额、用户数量、订单等

2). 用户端功能

微信登录 , 收件人地址管理 , 用户历史订单查询 , 菜品规格查询 , 购物车功能 , 下单 , 支付、分类及菜品浏览。

模块 描述
登录/退出 用户需要通过微信授权后登录使用小程序进行点餐
点餐-菜单 在点餐界面需要展示出菜品分类/套餐分类, 并根据当前选择的分类加载其中的菜品信息, 供用户查询选择
点餐-购物车 用户选中的菜品就会加入用户的购物车, 主要包含 查询购物车、加入购物车、删除购物车、清空购物车等功能
订单支付 用户选完菜品/套餐后, 可以对购物车菜品进行结算支付, 这时就需要进行订单的支付
个人信息 在个人中心页面中会展示当前用户的基本信息, 用户可以管理收货地址, 也可以查询历史订单数据

1.3用到的技术

1)用户层

用户层是指最终用户直接交互的界面或应用程序。本项目中在构建前端页面,我们会用到H5、Vue.js、ElementUI、apache echarts(展示图表)等技术。而在构建移动端应用时,我们会使用到微信小程序。

2)网关层

网关层值位于用户层和应用层之间的中间层,负责连接用户层和应用层,并提供额外的功能。

本项目使用到Nginx,Nginx是一个服务器,主要用来作为Http服务器,部署静态资源,访问性能高。在Nginx中还有两个比较重要的作用: 反向代理和负载均衡, 在进行项目部署时,要实现Tomcat的负载均衡,就可以通过Nginx来实现。

3)应用层

提供了不同应用程序之间进行通讯和数据交换的接口。

SpringBoot: 快速构建Spring项目, 采用 "约定优于配置" 的思想, 简化Spring项目的配置开发。

SpringMVC:SpringMVC是spring框架的一个模块,springmvc和spring无需通过中间整合层进行整合,可以无缝集成。

Spring Task: 由Spring提供的定时任务框架。

httpclient: 主要实现了对http请求的发送。

Spring Cache: 由Spring提供的数据缓存框架

JWT: 用于对应用程序上的用户进行身份验证的标记。

阿里云OSS: 对象存储服务,在项目中主要存储文件,如图片等。

Swagger: 可以自动的帮助开发人员生成接口文档,并对接口进行测试。

POI: 封装了对Excel表格的常用操作。

WebSocket: 一种通信网络协议,使客户端和服务器之间的数据交换更加简单,用于项目的来单、催单功能实现。

4)数据层

指存储和管理系统中的数据的层次。包括数据库和数据库管理系统等。

MySQL: 关系型数据库, 本项目的核心业务数据都会采用MySQL进行存储。

Redis: 基于key-value格式存储的内存数据库, 访问速度快, 经常使用它做缓存。

Mybatis: 本项目持久层将会使用Mybatis开发。

pagehelper: 分页插件。

spring data redis: 简化java代码操作Redis的API。

5)使用到的工具

git: 版本控制工具, 在团队协作中, 使用该工具对项目中的代码进行管理。

maven: 项目构建工具。

junit:单元测试工具,开发人员功能实现完毕后,需要通过junit对功能进行单元测试。

postman: 接口测工具,模拟用户发起的各类HTTP请求,获取对应的响应结果。

1.4项目的初始化

该项目采用前后端分离,这里重心放在后端开发上,前端环境直接导入即可·。

1)了解项目的整体结构:

项目中的几大模块

序号 名称 说明
1 sky-take-out maven父工程,统一管理依赖版本,聚合其他子模块
2 sky-common 子模块,存放公共类,例如:工具类、常量类、异常类等
3 sky-pojo 子模块,存放实体类、VO、DTO等
4 sky-server 子模块,后端服务,存放配置文件、Controller、Service、Mapper等

sky-common: 模块中存放的是一些公共类,可以供其他模块使用

名称 说明
constant 存放相关常量类
context 存放上下文类
enumeration 项目的枚举类存储
exception 存放自定义异常类
json 处理json转换的类
properties 存放SpringBoot相关的配置属性类
result 返回结果类的封装
utils 常用工具类

sky-pojo:** 模块中存放的是一些 entity、DTO、VO

名称 说明
Entity 实体,通常和数据库中的表对应
DTO 数据传输对象,通常用于程序中各层之间传递数据
VO 视图对象,为前端展示数据提供的对象
POJO 普通Java对象,只有属性和对应的getter和setter

sky-server: 模块中存放的是 配置文件、配置类、拦截器、controller、service、mapper、启动类等

名称 说明
config 存放配置类
controller 存放controller类
interceptor 存放拦截器类
mapper 存放mapper接口
service 存放service类
SkyApplication 启动类
2)使用Git版本控制

创建Git本地仓库(add添加到缓存区 commit添加到本地仓库)

创建Git远程仓库(push推送到远程仓库)

将后端初始环境代码推送到远程仓库(具体操作步骤我之前的笔记有提到)

3)数据库环境搭建

根据提供的sql文件导入对应的11张表。

具体的表说明:

序号 表名 中文名
1 employee 员工表
2 category 分类表
3 dish 菜品表
4 dish_flavor 菜品口味表
5 setmeal 套餐表
6 setmeal_dish 套餐菜品关系表
7 user 用户表
8 address_book 地址表
9 shopping_cart 购物车表
10 orders 订单表
11 order_detail 订单明细表

这里直接使用idea来操作数据库(个人感觉使用起来很方便,也可以使用和DataGrip,和idea同一家公司,ui看起来都很相似,使用起来也会顺手一些。)

4)前后端联调
确认MySQL密码,打开application.yml会看到配置如下:
spring:
  datasource:
    druid:
      driver-class-name: ${sky.datasource.driver-class-name}
      username: ${sky.datasource.username}
      password: ${sky.datasource.password}
发现使用的是${...}:作用其实和注解@Value("${user.age}")一样,都是从配置文件中读取配置.

#启动之前,记得确认数据库配置:application-dev.yml
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    #改成自己的数据库密码
    password: 123456
编译父工程sky-take-out,保证依赖的所有jar包都下载成功

启动SkyApplication,直接进行前后端联调测试即可:http://localhost(这里你登陆时会发现使用的还是之前瑞吉外卖的图标哈哈哈哈)
5)JWT令牌技术
JWT全称:JSON Web Token  (官网:https://jwt.io/) 

  定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

  简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了。

第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{"alg":"HS256","type":"JWT"}

第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{"id":"1","username":"Tom"}

第三部分:Signature(签名),防止Token被篡改确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来:HS256(header+payload, secret)

在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:
1. 在登录成功之后,要生成令牌。
2. 每一次请求当中,要接收令牌并对令牌进行校验。
6)Nginx反向代理和负载均衡

在前后端联调时我们发现前端发出的请求和后端接口中的地址不一样。这里使用到了nginx反向代理

nginx 反向代理的好处:

 - 提高访问速度
因为nginx本身可以进行缓存,如果访问的同一接口,并且做了数据缓存,nginx就直接可把数据返回,不需要真正地访问服务端,从而提高访问速度。

 - 进行负载均衡
所谓负载均衡,就是把大量的请求按照我们指定的方式均衡的分配给集群中的每台服务器。

 - 保证后端服务安全
因为一般后台服务地址不会暴露,所以使用浏览器不能直接访问,可以把nginx作为请求访问的入口,请求到达nginx后转发到具体的服务中,从而保证后端服务的安全。
7)MD5加密登录

员工表中的密码是明文存储,安全性太低。

--->修改数据库中明文密码,改为MD5加密后的密文:

//后期进行md5加密,然后再进行比对
password = DigestUtils.md5DigestAsHex(password.getBytes());
8)导入接口文档
第一步:定义接口,确定接口的路径、请求方式、传入参数、返回参数。

第二步:前端开发人员和后端开发人员并行开发,同时,也可自测。

第三步:前后端人员进行连调测试。

第四步:提交给测试人员进行最终测试。
    
操作步骤:
    1). 从资料中找到项目接口文件
    2). 导入到YApi平台 在YApi平台创建出两个项目,选择苍穹外卖-管理端接口.json导入
9)Swagger

Swagger 是一个规范和完整的框架,用于生成、描述、调用和可视化 RESTful 风格的 Web 服务

它的主要作用是:

  1. 使得前后端分离开发更加方便,有利于团队协作

  2. 接口的文档在线自动生成,降低后端开发人员编写接口文档的负担

  3. 功能测试

    Spring已经将Swagger纳入自身的标准,建立了Spring-swagger项目,现在叫Springfox。通过在项目中引入Springfox ,即可非常简单快捷的使用Swagger。

目前,一般都使用knife4j框架。

使用步骤:

1.导入knife4j的maven坐标

<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>knife4j-spring-boot-starter</artifactId>
</dependency>

2.配置类中加入knife4j

-WebMvcConfiguration.java

/**
     * 通过knife4j生成接口文档
     * @return
*/
    @Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }

3.设置静态资源映射

-WebMvcConfiguration.java

/**
     * 设置静态资源映射
     * @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}

4.访问测试

接口文档访问路径:http://ip:port/doc.html

在本项目中也就是: http://localhost:8080/doc.html

常用注解:

注解 说明
@Api 用在类上,例如Controller,表示对类的说明
@ApiModel 用在类上,例如entity、DTO、VO
@ApiModelProperty 用在属性上,描述属性信息
@ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用

注:

Yapi是开发阶段使用的工具

Swagger是在开发阶段使用的工具,帮助后端人员做后端的接口测试

ThreadLocal

2.1ThreadLocal介绍

ThreadLocal 并不是一个Thread,而是Thread的局部变量。

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

常用方法:

public void set(T value) 	设置当前线程的线程局部变量的值

public T get() 		        返回当前线程所对应的线程局部变量的值

public void remove()        移除当前线程的线程局部变量
2.2实现

通过代码验证:当tomcat接受到请求后,拦截器 -> Controller -> Service -> Mapper(是不是同一个线程)

通过线程ID来验证

   System.out.println("当前线程ID:" + Thread.currentThread().getId());

初始工程中已经在sky-common模块封装了 ThreadLocal 操作的工具类:

package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
    
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }
    
    public static Long getCurrentId() {
        return threadLocal.get();
    }
    
    public static void removeCurrentId() {
        threadLocal.remove();
    }

}

在拦截器中解析出当前登录员工id,并放入线程局部变量中:

在sky-server模块中,拦截器:

package com.sky.interceptor;

//jwt令牌校验的拦截器
@Component

@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {

      @Autowired
      private JwtProperties jwtProperties;

       /**
        * 校验jwt
        * @param request
        * @param response
        * @param handler
        * @return
        * @throws Exception
        */
   public boolean preHandle(HttpServletRequest request,
                       HttpServletResponse response, 
                       Object handler) throws Exception {


 //2、校验令牌
 try {
     //.................
     Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
     Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
     log.info("当前员工id:", empId);
     /////将用户id存储到ThreadLocal////////
     BaseContext.setCurrentId(empId);
     ////////////////////////////////////
     //3、通过,放行
     return true;
 } catch (Exception ex) {
     //......................
 }
}
//请求结束之前执行,清理ThreadLocal存储的ID
  public void afterCompletion(HttpServletRequest request, 
                              HttpServletResponse response, 
                              Object handler, Exception ex) throws Exception {
      BaseContext.removeCurrentId();
  }
}

项目中在分页查询时会编写大量重复的代码,所以使用分页查询插件来完成

保证了代码的简洁和优雅(手动狗头)

PageHelper

3.1介绍

mybatis 的分页插件 PageHelper 来简化分页代码的开发。

底层基于 mybatis 的拦截器实现

3.2使用

​```xml

com.github.pagehelper
pagehelper-spring-boot-starter


员工分页查询

/**
* 分页查询
*
* @param employeePageQueryDTO
* @return
*/
public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
// select * from employee limit 0,10
//开始分页查询,获取起始页和页面大小
PageHelper.startPage(employeePageQueryDTO.getPage(),
employeePageQueryDTO.getPageSize());//调用方法

    long total = page.getTotal();
    List<Employee> records = page.getResult();

    return new PageResult(total, records);

}


```yml
//1.可以在 src/main/resources/mapper/EmployeeMapper.xml 中动态编写SQL:
 xml放置位置可以在配置文件中自行指定
 mybatis:
   mapper-locations: classpath:mapper/*.xml #xml文件位置
   type-aliases-package: com.sky.entity #实体类所在包

2、方法返回类型为Page<Employee>,使用resultType指定时写Page中泛型的类型Employee即可
3、com.sky.entity.Employee:包名+类名比较繁琐,可以进行简化:在配置中指定包名
    
<!--完整包名+类名-->
<!-- <select id="pageQuery" resultType="com.sky.entity.Employee"> -->
<!--在配置文件中指定包名后,只写类名即可:Employee, employee都可以-->   
<select id="pageQuery" resultType="Employee">
    select * from employee
    <where>
        <if test="name != null and name != ''">
            and name like concat('%',#{name},'%')
        </if>
    </where>
    order by create_time desc
</select>

完善时间显示

方式一:

在Employee类的属性上加上注解,对日期进行格式化。

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime; 
但这种方式,需要在每个时间属性上都要加上该注解,使用较麻烦,不能全局处理。

方式二(推荐):
在WebMvcConfiguration中扩展SpringMVC的消息转换器,使用自定义的JacksonObjectMapper统一对日期类型进行格式处理:
其中JacksonObjectMapper是通用的类已经定义好,直接使用即可。

package com.sky.json;

  /**
   * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
   * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
   * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
   */
   public class JacksonObjectMapper extends ObjectMapper {

   public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
   //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
   public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
   public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

   //省略后续代码
   }

   
     /**
      * 扩展Spring MVC框架的消息转化器
      * @param converters
      */
      protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
      log.info("扩展消息转换器...");
      //创建一个消息转换器对象,注意不要导错包是Jackson2Http
      MappingJackson2HttpMessageConverter converter = 
             new MappingJackson2HttpMessageConverter();
      //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
      converter.setObjectMapper(new JacksonObjectMapper());
      //将自己的消息转化器加入容器中
      converters.add(0, converter);
      }

AOP切面编程

4.1介绍

我们使用AOP切面编程,实现功能增强,来完成公共字段自动填充功能。
AOP(面向切面编程):

重要名词:

  • 通知Advice(方法中的共性功能),

  • 切入点Pointcut(哪些方法),

  • 切面Aspect(描述切入点和通知位置关系),

  • 通知类型(前置,后置:方法前边加还是后边加)

    在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。
    ★ 技术点:枚举、注解、AOP、反射.

4.2操作步骤

1)自定义注解

  •   /**
       * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
       */
       @Documented //注解是否将包含在JavaDoc中
       @Target(ElementType.METHOD) //指定注解可以加在什么地方(类,方法,成员变量)
       @Retention(RetentionPolicy.RUNTIME) //定义注解的生命周期
       public @interface AutoFill {
      
       //数据库操作类型:UPDATE INSERT
       OperationType value() default OperationType.INSERT;
       //使用value时候,@AutoFill(value = OperationType.UPDATE)
       }
      
    其中RetentionPolicy的不同策略对应的生命周期如下:
        
    
    - RetentionPolicy.SOURCE : 仅存在于源代码中,编译阶段会被丢弃,不会包含于class字节码文件中。@Override, @SuppressWarnings都属于这类注解。
    
    - RetentionPolicy.CLASS : 默认策略,在class字节码文件中存在,在类加载时被丢弃,运行时无法获取到
    
    - RetentionPolicy.RUNTIME : 始终不会丢弃,可以使用反射获得该注解的信息。自定义的注解最常用的使用方式。
    

    2)枚举定义

package com.sky.enumeration;
  /**
   * 数据库操作类型
   */
   public enum OperationType {
     /**
      * 更新操作
      */
      UPDATE,
     /**
      * 插入操作
      */
      INSERT
 }

3)切入点表达式

要进行增强的方法的描述方式

//1.execution([访问修饰符]  返回值  包名.类/接口名.方法名(参数) [异常名])
execution(public User com.itheima.service.UserService.findById(int))
//2.当方法上使用指定注解时    
@annotation(com.sky.annotation.AutoFill)

4)自定义切面类

4、自定义切面类

/**
前置通知,在通知中进行公共字段的赋值
*/
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
log.info("开始进行公共字段自动填充...");

//获取到当前被拦截的方法上的数据库操作类型
MethodSignature signature = (MethodSignature) joinPoint.getSignature();//方法签名对象
AutoFill autoFill = signature.getMethod()
    .getAnnotation(AutoFill.class);//获得方法上的注解对象
OperationType operationType = autoFill.value();//获得数据库操作类型

//获取到当前被拦截的方法的参数--实体对象
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) {
    return;
}

//按照约定:新增或者修改实体对象放到第一个参数
Object entity = args[0];

//准备赋值的数据
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();

//根据当前不同的操作类型,为对应的属性通过反射来赋值
if (operationType == OperationType.INSERT) {
    //为4个公共字段赋值
    try {
        //通过方法名和参数类型获取定义好的方法:setCreateTime
        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) {
    e.printStackTrace();
}
```

} else if (operationType == OperationType.UPDATE) {
    //为2个公共字段赋值
    try {
        Method setUpdateTime = entity.getClass().getDeclaredMethod(
                AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
        Method setUpdateUser = entity.getClass().getDeclaredMethod(
                AutoFillConstant.SET_UPDATE_USER, Long.class);

```
    //通过反射为对象属性赋值
    setUpdateTime.invoke(entity, now);
    setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
    e.printStackTrace();
        }
    }
} 

5)加入AutoFill注解
在Mapper接口的方法上加入 AutoFill 注解

@Mapper
public interface CategoryMapper {
     /**
      * 插入数据
      @param category
       */
      @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
     " VALUES" +
     " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
      @AutoFill(value = OperationType.INSERT)
      void insert(Category category);
      /**
       * 根据id修改分类
       @param category
       */
       @AutoFill(value = OperationType.UPDATE)
       void update(Category category);
 }

具体部分在前面的一篇博客里有说明。

阿里云OSS存储

这部分在Java_Web课程中有详细讲解过,所以这个课程中没有详细讲解

5.1阿里云OSS简介

-- 阿里云对象存储服务(Object Storage Service,简称OSS)为您提供基于网络的数据存取服务。使用OSS,您可以通过网络随时存储和调用包括文本、图片、音频和视频等在内的各种非结构化数据文件。
-- 阿里云OSS将数据文件以对象(object)的形式上传到存储空间(bucket)中。

1)打开https://www.aliyun.com/ ,申请阿里云账号并完成实名认证。

2)参考文档官方快速入门

3)获取AccessKeyId

5.2操作步骤

1)定义OSS相关配置

application-dev.yml

#sky:
  alioss:
    endpoint: oss-cn-hangzhou.aliyuncs.com
    access-key-id: LTAI5tPeFLzsPPT8gG3LPW64
    access-key-secret: U6k1brOZ8gaOIXv3nXbulGTUzy6Pd7
    #改成自己创建桶的名称
    bucket-name: sky-take-out
#application.yml
spring:
  profiles:
    active: dev    #设置环境
sky:
  alioss:
    endpoint: ${sky.alioss.endpoint}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket-name: ${sky.alioss.bucket-name}

2)读取OSS配置

在sky-common模块中

package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.alioss")
@Data
public class AliOssProperties {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

}

3)生成OSS工具类对象

在sky-server

/**
配置类,用于创建AliOssUtil对象
*/
@Configuration
@Slf4j
public class OssConfiguration {

@Bean
@ConditionalOnMissingBean //检查IoC容器,如果没有此对象再创建
public AliOssUtil aliOssUtil(@Autowired AliOssProperties aliOssProperties){
    log.info("开始创建阿里云文件上传工具类对象:{}",aliOssProperties);
    return new AliOssUtil(aliOssProperties.getEndpoint(),
            aliOssProperties.getAccessKeyId(),
            aliOssProperties.getAccessKeySecret(),
            aliOssProperties.getBucketName());
    }
}

AliOssUtil.java在sky-common模块中定义即可

这个是阿里云官方提供的API,只需完成响应的配置即可。

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {

    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;
    
    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {
    
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
    
        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }
    
        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);
    
        log.info("文件上传到:{}", stringBuilder.toString());
    
        return stringBuilder.toString();
    }

}
 

4)定义文件上传接口

/**
 通用接口
*/
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {

@Autowired
private AliOssUtil aliOssUtil;

/**
文件上传
@param file
@return
*/
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file){
log.info("文件上传:{}",file);

try {
    //原始文件名
    String originalFilename = file.getOriginalFilename();
    //截取原始文件名的后缀:abc.dfdfdf.png
    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.getMessage());
  }

return Result.error(MessageConstant.UPLOAD_FAILED);
   }
}

事务处理

6.1事务管理

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败。

怎么样来控制这组操作,让这组操作同时成功或同时失败呢?此时就要涉及到事务的具体操作了。

事务的操作主要有三步:

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  2. 提交事务(这组操作全部成功后,提交事务):commit ;
  3. 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;

在方法运行之前,开启事务,如果方法成功执行,就提交事务,如果方法执行的过程当中出现异常了,就回滚事务。

思考:开发中所有的业务操作,一旦我们要进行控制事务,是不是都是这样的套路?

答案:是的。

所以在spring框架当中就已经把事务控制的代码都已经封装好了,并不需要我们手动实现。我们使用了spring框架,我们只需要通过一个简单的注解@Transactional就搞定了。

6.2具体操作
按照产品原型中的要求,删除实现比较复杂:

- 起售中的菜品(status=1)不能删除
- 被套餐关联的菜品(setmeal_dish表)不能删除
- 删除菜品后,关联的口味数据(dish_flavor表)也需要删除掉

//1、检查菜品的状态,如果启售不能删除

//2、检查菜品是否被套餐引用,如果引用不能删除

//3、检查通过后,删除菜品基本信息和口味信息
@Transactional//事务注解
public void deleteBatch(List<Long> ids) {
        //判断当前菜品是否能够删除---是否存在起售中的菜品??
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);//后绪步骤实现
            if (dish.getStatus().equals(StatusConstant.ENABLE)) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }
        

        //上述检查方式可以进行如下优化
        //另外一种判断方式:SELECT id FROM dish WHERE id IN (51, 52, 53) AND STATUS = 1
         //select * from dish where id = 51
        //select * from dish where id = 52
         //select * from dish where id = 53
    
        //for循环外边
        //List<Long> = SELECT id FROM dish WHERE id IN (51, 52, 53) and status = 1
        //可自行实现
    
        //判断当前菜品是否能够删除---是否被套餐关联了??
        //select setmeal_id from setmeal_dish where dish_id IN (52,54,56)
        List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setmealIds != null && setmealIds.size() > 0) {
            //当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }
    
        //删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);//后绪步骤实现
            //删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);//后绪步骤实现
        }
    }

Redis

7.1Redis简介
 Redis是一个基于内存的key-value结构数据库。Redis 是互联网技术领域使用最为广泛的存储中间件。
     
**官网:**https://redis.io
**中文网:**https://www.redis.net.cn/
**key-value结构存储:**

**主要特点:**

- 基于内存存储,读写性能高  
- 适合存储**热点数据**(热点商品、资讯、新闻)
- 企业应用广泛
  Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供的数据是可以达到100000+的QPS(每秒内查询10W次)。它存储的value类型比较丰富,也被称为结构化的NoSql数据库。
   NoSql(Not Only SQL),不仅仅是SQL,泛指**非关系型数据库**。NoSql数据库并不是要取代关系型数据库,而是关系型数据库的补充。

**关系型数据库(RDBMS):**

- Mysql
- Oracle
- DB2
- SQLServer

**非关系型数据库(NoSql):**

- Redis
- Mongo db
- MemCached
  Redis安装包分为windows版和Linux版:

- Windows版下载地址:https://github.com/microsoftarchive/redis/releases
- Linux版下载地址: https://download.redis.io/releases/ 

Redis的安装
1)在Windows中安装Redis(项目中使用)    
Redis的Windows版属于**绿色软件**,直接解压即可使用。
redis-cli.exe: Redis客户端redis-client

7.2Redis服务启动与其停止
服务启动命令
   #手动指定配置文件
redis-server.exe redis.windows.conf

Redis服务默认端口号为 **6379** ,通过快捷键**Ctrl + C** 即可停止Redis服务

当Redis服务启动成功后,可通过客户端进行连接。

客户端连接命令 : redis-cli.exe
通过redis-cli.exe命令默认连接的是本地的redis服务,并且使用默认6379端口。也可以通过指定如下参数连接:
redis-cli.exe -h 127.0.0.1 -p 6379 -a 123456

- -h ip地址
- -p 端口号
- -a 密码(如果需要)

注:如果感觉麻烦可以在桌面编写一个bat脚本实现

先在桌面创建一个txt文件,输入以下代码,然后修改后缀为.bat

start cmd /k "cd /d D:\StudyingTools\Redis-x64-3.2.100&&redis-server.exe redis.windows.conf"
//磁盘路径为自己Redis安装路径
7.3Redis客户端图形化工具以及数据类型

这部分内容在我之前的笔记里有写到,这里就不详细说明了。

7.4Spring Data Redis使用方式
Redis 的 Java 客户端很多,常用的几种:

- Jedis: 在java中操作Redis

- Lettuce

- Spring Data Redis

Spring 对 Redis 客户端进行了整合,提供了 Spring Data Redis,在Spring Boot项目中还提供了对应的Starter,即 spring-boot-starter-data-redis。

重点学习Spring Data Redis。

1).导入Spring Boot提供了对应的Starter,maven坐标:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

- ValueOperations:string数据操作

- SetOperations:set类型数据操作

- ZSetOperations:zset类型数据操作

- HashOperations:hash类型的数据操作

- ListOperations:list类型的数据操作

2). 配置Redis数据源

#sky:
  redis:
    host: localhost
    port: 6379
    #设置密码,没有设置则注释掉
    password: 123456
    database: 0

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

#可以使用select 切换不同的数据库
select 0
select 1

在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). 编写配置类,创建RedisTemplate对象

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }

}

/*********************************************************************************/
当前配置类不是必须的,因为 Spring Boot 框架会自动装配 RedisTemplate 对象:RedisAutoConfiguration,但是默认的key序列化器为

JdkSerializationRedisSerializer,导致我们存到Redis中后的数据和原始数据有差别,故设置为StringRedisSerializer序列化器。
    
//jar包中RedisAutoConfiguration类的源码
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
   RedisTemplate<Object, Object> template = new RedisTemplate<>();
   template.setConnectionFactory(redisConnectionFactory);
   return template;
}

4). 通过RedisTemplate对象操作Redis

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

    @Test //注意:使用长的import org.junit.jupiter.api.Test;
    public void testRedisTemplate(){
        System.out.println(redisTemplate);
        //string数据操作
        ValueOperations valueOperations = redisTemplate.opsForValue();
        //hash类型的数据操作
        HashOperations hashOperations = redisTemplate.opsForHash();
        //list类型的数据操作
        ListOperations listOperations = redisTemplate.opsForList();
        //set类型数据操作
        SetOperations setOperations = redisTemplate.opsForSet();
        //zset类型数据操作
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }

}
7.5数据同步

为了保证数据库和Redis中的数据保持一致,

修改管理端接口 DishController的相关方法,加入清理缓存逻辑。

注意:在使用缓存过程中,要注意保证数据库中的数据和缓存中的数据一致

如果MySQL中的数据发生变化,需要及时清理缓存数据。否则就会造成缓存数据与数据库数据不一致的情况。
抽取清理缓存的方法:

在管理端DishController中添加
新增菜品优化
    @PostMapping
    @ApiOperation("新增菜品")
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavor(dishDTO);

        //==========================新增代码=====================
        //清理缓存数据
        String key = "dish_" + dishDTO.getCategoryId();
        cleanCache(key);
        return Result.success();
    }

菜品批量删除优化
//==========================新增代码=====================
        //将所有的菜品缓存数据清理掉,所有以dish_开头的key
        cleanCache("dish_*");
修改菜品优化
 //==========================新增代码=====================
        cleanCache("dish_" + dishDTO.getCategoryId());
菜品起售停售优化
//==========================新增代码=====================
        //将所有的菜品缓存数据清理掉,所有以dish_开头的key
        cleanCache("dish_*");

Spring Cache框架

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

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

  • EHCache

  • Caffeine

  • Redis(常用)

    起步依赖:

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

常用的注解

注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类上
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除

在spring boot项目中,

使用缓存技术只需在项目中导入相关缓存技术的依赖包,并在启动类上使用@EnableCaching开启缓存支持即可。

使用Redis作为缓存技术,只需要导入Spring data Redis的maven坐标即可。

1)@EnableCaching注解

创建名为spring_cache_demo数据库,将springcachedemo.sql脚本直接导入数据库中。
引导类上加@EnableCaching:

@Slf4j
@SpringBootApplication
@EnableCaching//开启缓存注解功能
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
        log.info("项目启动成功...");
    }
}

配置文件中检查数据库和redis的账号密码:

#数据库密码和Redis密码
spring:
  datasource:
    druid:
      username: root
      password: root
  redis:
    host: localhost
    port: 6379
    password: 123456
2)@CachePut注解

​ 作用: 将方法返回值,放入缓存

value: 缓存的名称, 每个缓存名称下面可以有很多key

key: 缓存的key  ----------> 支持Spring的表达式语言SPEL语法

在save方法上加注解@CachePut

   /*
	 CachePut:将方法返回值放入缓存
	 value:缓存的名称,每个缓存名称下面可以有多个key
	 key:缓存的key
	*/
	@PostMapping
    //@CachePut(value = "userCache", key = "#resutl.id")
    @CachePut(value = "userCache", key = "#user.id")//key的生成:userCache::1
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

user.id : #user指的是方法形参的名称, id指的是user的id属性 , 也就是使用user的id属性作为key ;
result.id : #result代表方法返回值,该表达式 代表以返回对象的id属性作为key ;

3) @Cacheable注解

作用: 在方法执行前,spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中

value: 缓存的名称,每个缓存名称下面可以有多个key

key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在getById上加注解@Cacheable

   /**
	* Cacheable:在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,	  *调用方法并将方法返回值放到缓存中
	* value:缓存的名称,每个缓存名称下面可以有多个key
	* key:缓存的key
	*/
	@GetMapping
    @Cacheable(cacheNames = "userCache",key="#id")
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }
4) @CacheEvict注解

作用: 清理指定缓存

value: 缓存的名称,每个缓存名称下面可以有多个key

key: 缓存的key ----------> 支持Spring的表达式语言SPEL语法

在 delete 方法上加注解@CacheEvict

    @DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")//删除某个key对应的缓存数据
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }
    @DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)//删除userCache下所有的缓存数据
    public void deleteAll(){
        userMapper.deleteAll();
   }

以上内容仅供个人学习使用
:在写这篇文章时参考了黑马老师所提供的每日讲义

以及大佬的博客:原文链接:https://blog.csdn.net/DU1149507047/article/details/132511480