苍穹外卖开发记录以及完善Day1-Day6

开发技术栈使用版本

  • Java版本:17

  • SpringBoot版本:2.7.3

  • Maven版本:3.6.1

  • 数据库版本:8.0.39

Day01-06Git进行版本控制

方法一:直接在idea里进行操作

点击上方菜单的VCS ,点击Create Git Reposity

方法二:在命令行敲代码

详细去看git常用命令

Day01-08熟悉代码

JWT(JSON Web Token)是一种用于在网络应用环境中安全地传递信息的标准。它是一种自包含的令牌,用于在客户端和服务器之间传递信息,并且可以用于身份验证和授权。

JWT的主要特点包括:

  1. 自包含:JWT的内容包括了所有需要的信息,通常是用户的身份信息和权限信息。它避免了服务器对会话状态的依赖,从而减轻了服务器负担。

  2. 三部分组成:JWT由三部分组成:

    • 头部(Header):包含令牌的类型(通常是JWT)和签名算法(如HS256或RS256)。

    • 有效载荷(Payload):包含实际的声明(Claims),这些声明是关于实体(通常是用户)及其他数据的。声明可以是注册声明(预定义的),公共声明或私有声明。

    • 签名(Signature):用来验证令牌的真实性。它是通过将头部和有效载荷编码后与一个密钥一起使用签名算法生成的。验证时,服务器可以通过重新生成签名来确认令牌的有效性和完整性。

  3. 如何工作

    • 客户端向认证服务器请求认证(如登录)。

    • 认证服务器验证用户身份,并生成一个JWT。

    • 服务器将JWT返回给客户端。

    • 客户端在后续请求中将JWT附加到HTTP头部(通常是Authorization字段)。

    • 服务器接收JWT,验证其签名,解码载荷,从而确认用户的身份和权限。

JWT广泛应用于API认证和用户身份验证,特别是在分布式系统和微服务架构中。

在Spring Boot中使用JWT通常涉及以下几个步骤:

1. 添加依赖

首先,你需要在pom.xml中添加相关的JWT库依赖。例如,可以使用jjwt库:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
</dependency>

2. 创建自定义JWT拦截类

创建一个工具类来生成和解析JWT令牌。以下是苍穹外卖的JWT拦截器,配置通过创建配置类被spring管理,可以实现解耦

package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 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 {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
//          存储用户id进入ThreadLocal
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

3. 在配置类的WebMvcConfiguration中注册拦截器

该类继承自WebMvcConfigurationSupport

类继承自WebMvcConfigurationSupport

WebMvcConfigurationSupport 是 Spring MVC 提供的用于配置 Web MVC 的类,它提供了一些方法用于配置 Spring MVC 的行为。通过继承 WebMvcConfigurationSupport 类,可以定制化配置 Spring MVC 的各种功能,如视图解析器、消息转换器、拦截器等。

package com.sky.config;

import com.sky.interceptor.JwtTokenAdminInterceptor;
import com.sky.interceptor.JwtTokenUserInterceptor;
import com.sky.json.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.cbor.MappingJackson2CborHttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;

import java.util.List;

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;

    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }

    /**
     * 通过knife4j生成接口文档
     *
     * @return
     */
    @Bean
    public Docket docket() {
        log.info("开始生成接口文档");
        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;
    }

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


    /**
     * 处理时间格式
     *
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {

        log.info("扩展消息转换器:转换时间格式");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //设置消息转换器,可将Java对象转换为Json
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自定义的消息转换器放入spring mvc中
        converters.add(0, converter);
    }
}

Day01-09Nignx

Nginx 是一种高性能的开源 Web 服务器和反向代理服务器,也是一个 IMAP/POP3/SMTP 代理服务器。它最初由俄罗斯程序员 Igor Sysoev 开发,并在2004年首次发布。由于其高效的事件驱动架构,Nginx 能够处理大量的并发连接,这使得它特别适合处理高并发请求的场景,如大型网站和高流量的应用程序。

Nginx 的主要功能

  1. 静态内容服务:Nginx 能够非常高效地提供静态文件(如 HTML、CSS、图像和 JavaScript 等),其性能表现优于许多传统的 Web 服务器。

  2. 反向代理:Nginx 作为反向代理服务器时,接收客户端的请求,并将这些请求转发到后端服务器处理,然后将处理结果返回给客户端。这使得它可以实现负载均衡、缓存加速、SSL加密卸载等功能。

  3. 负载均衡:Nginx 可以将流量分配到多个后端服务器上,从而提高应用的可扩展性和可靠性。它支持多种负载均衡算法,如轮询、最少连接、IP哈希等。

  4. 缓存:Nginx 支持缓存静态内容和反向代理结果,这有助于减少后端服务器的负载,并加速用户的访问速度。

  5. SSL/TLS 终止:Nginx 可以处理 SSL/TLS 加密通信,从而减少后端服务器的负担。

什么是反向代理?

反向代理是代理服务器的一种形式,它位于客户端和一台或多台服务器之间,接收来自客户端的请求并将它们转发到后端服务器。反向代理服务器会隐藏后端服务器的存在和特性,使得客户端只需与反向代理服务器进行交互。Nginx 作为反向代理服务器的优势在于其处理并发请求的能力和灵活的配置选项。

反向代理的主要作用

  1. 负载均衡:将客户端请求分发到多台后端服务器,避免单一服务器的负载过重,提高系统的可用性。

  2. 安全性增强:隐藏后端服务器的 IP 地址和其他信息,防止直接的外部攻击,同时可以配置 SSL 加密,保护数据传输安全。

  3. 缓存加速:通过缓存来自后端服务器的响应,减少服务器负载,加快响应速度。

  4. 压缩和解压:在请求或响应时进行压缩或解压,减少带宽使用。

  5. 内容过滤和修改:根据一定的规则过滤请求和响应内容,或者对其进行修改。

Nginx在Spring Boot中的配置

  1. 配置nginx.conf

  2. nginx默认负载均衡是轮询

关闭nginx

如果使用cmd命令窗口启动nginx,关闭cmd窗口是不能结束nginx进程的,可使用两种方法关闭nginx

(1)输入nginx命令  nginx -s stop(快速停止nginx)  或  nginx -s quit(完整有序的停止nginx)
​
(2)使用taskkill   taskkill /f /t /im nginx.exe

————————————————

Day02-02新增员工

使用Dto的原因

  1. 封装参数可以方便校验 在参数类的变量上添加注解 就可以很方便完成数据校验 而且代码可读性很高

  2. 如果你用实体类来传参 不需要的变量是对性能的浪费

  3. 如果接口需要的参数很少并且不需要做数据校验的时候 也可以不做封装直接传参

  4. DTO精确封装,只封装前端提交的有用的信息,并且过滤敏感信息

  • 虽然MybatisPlus已经把基础的CURD封装好了

  • 但是这次苍穹外卖开发不使用MybatisPlus,使用注解和xml开发

  • 虽然这些sql很简单,但是有可能会遇到复杂度sql

  • 简单的写在注解里,复杂的写在xml里

设置注解内SQL语句的自动补全(可自行搜索)

简单总结:

  1. Idea导入对应数据库

  2. 设置全局或者本项目SQL方言(SQL Dialect)为MySQL

小问题

java.sql.SQLException: Column count doesn't match value count at row 1

原因sql语句内的属性没有对齐

    @Select("insert into employee (name, username, password, phone, sex, id_number, " +
            "status, create_time, update_time, create_user, update_user)" +
            " values " +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber}," +
            "#{status},#{createTime},#{updateTime},#{createUser},#{updateUser})")
    void insert(Employee employee);

在全局异常里设置处理SQL异常

在大多数情况下,推荐直接尝试插入数据,然后处理可能出现的异常。这种方法被称为“乐观锁”策略:

  1. 直接插入数据:无需先查询,直接尝试插入。

  2. 捕获异常:如果插入违反了数据库约束(如唯一性约束),捕获异常并处理。

  3. 高效处理:通过异常处理机制,可以根据业务需求给出用户友好的提示信息,如“用户名已存在”。

这种方法不仅减少了不必要的数据库查询,还能保证数据的一致性,是一种更高效、更可靠的方式来处理并发数据插入。

在这里我使用了PatternMatcher进行匹配,这么写比原来的写法更健壮,而且可以处理其他约束产生的异常

package com.sky.handler;

import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import java.sql.SQLIntegrityConstraintViolationException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    /**
     * 处理SQL异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex){
        String message = ex.getMessage();
//        更健壮的正则处理,防止mysql版本更新
        if (message.contains("Duplicate entry")) {
            Pattern pattern = Pattern.compile("Duplicate entry '(.*?)' for key '(.*?)'");
            Matcher matcher = pattern.matcher(message);
            if (matcher.find()) {
                String duplicateValue = matcher.group(1);
                String key = matcher.group(2);
                String msg;

                switch (key) {
                    case "idx_username":
                        msg = "用户名为" + duplicateValue + MessageConstant.ALREADY_EXISTS;
                        break;
                    case "unique_idNumber":
                        msg = "身份证为" + duplicateValue + MessageConstant.ALREADY_EXISTS;
                        break;
                    // 其他唯一性约束处理
                    default:
                        msg = "值为" + duplicateValue + MessageConstant.ALREADY_EXISTS;
                }

                return Result.error(msg);
            }
        }
        return Result.error(MessageConstant.UNKNOWN_ERROR);
    }

}

注意:唯一性约束必须在数据库里已经建立才能在代码中有效地捕获并处理这些约束引发的异常。 另外除了 SQLIntegrityConstraintViolationException,还可以针对不同的 SQL 异常类型进行处理,如 DataIntegrityViolationExceptionSQLSyntaxErrorException 等,提供更精细化的错误提示。

Day02-05ThreadLocal

利用ThreadLocal设置为新增员工设置创建人,修改人

测试成功

不过后面肯定要用Aop,给全局设置一个拦截器

Day02-06员工分页查询

本质:单表查询

分页查询统一封装为pageResult

主要属性

  • total 总记录数

  • records 当前页数据集合

实现Serializable接口的类可以被序列化,即可以将对象转换为字节序列,以便在网络上传输或保存到文件中。以便进行持久化或网络传输

PageHelpr插件需要的返回值是固定的,Page<查询对象>

Page还继承了ArrayList

用Mybatis提供的<where>标签

MySQL提供的函数concat

不用在写limit关键字,因为PageHelper已经动态去拼接了这一块

员工分页查询步骤

  1. 现在controller里获取请求

  2. 在service编写业务,分好页,利用PageHelper

  3. 在mapper创建对应方法,因为这次SQL比较复杂,所以写在xml

  4. 在xml编写分页查询的sql代码

    1. 需要在sql里使用<if>标签写判断

  5. 把service返回的page对象封装到PageResult对象,然后返回

Day02-09消息拓展

问题:如何把时间以正确格式展示❓

解决方式:

方式一:在属性上加入注解,对日期进行格式化,需要逐一添加,不建议使用

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

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

    /**
     * 处理时间格式
     * @param converters
     */
    @Override
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器:转换时间格式");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //设置消息转换器,可将Java对象转换为Json
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自定义的消息转换器放入spring mvc中
        converters.add(0, converter);
    }

简单理解:

相当于默认返回数据时候会经过一个过滤器。然后你自己创建一个过滤器,定义特殊规则,就是把时间类进行格式化。然后应用这个过滤器

黑马的代码在 JacksonObjectMapper 中使用 SimpleModule 来注册 LocalDateTime 等类型的序列化和反序列化器。事实上,Jackson 提供了一个现成的 JavaTimeModule,我们可以直接使用它来处理 Java 8 的日期时间类型。这可以简化代码。

public JacksonObjectMapper() {
    super();
    this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

    // 注册 JavaTimeModule
    JavaTimeModule javaTimeModule = new JavaTimeModule();
    this.registerModule(javaTimeModule);

    // 如果需要自定义格式,可以手动设置格式化器
    javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
    javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
    // 同理,添加 LocalDate 和 LocalTime 的格式化器
    javaTimeModule.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
    javaTimeModule.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)));
}

扩展消息转换器的常见作用

扩展消息转换器在 Spring MVC 中有许多用途,常见的包括:

  • 自定义序列化和反序列化:如你当前实现的,为特定数据类型(如 Java 8 的日期时间类型)定义自定义的序列化和反序列化逻辑。

  • 处理非标准的 JSON 结构:如果你需要处理特定格式的 JSON 结构,或者需要对接第三方 API,可能需要自定义消息转换器以正确处理这些 JSON 格式。

  • 支持额外的媒体类型:默认的 Spring MVC 配置只支持一些常见的媒体类型(如 application/jsonapplication/xml 等)。通过扩展消息转换器,你可以为应用程序添加对自定义媒体类型(如 application/vnd.api+json)的支持。

  • 处理其他格式的数据:除了 JSON 和 XML,你还可以添加对其他格式(如 CSVYAML 等)的支持,使用自定义的消息转换器将这些格式的数据与 Java 对象之间进行转换。

  • 数据压缩与解压缩:在一些特定场景下,你可能需要对响应的数据进行压缩(如 gzip),或者对请求的数据进行解压缩。通过自定义消息转换器,可以在数据传输过程中直接处理这些压缩逻辑。

通过扩展消息转换器,Spring MVC 的数据转换能力可以得到显著增强,使得应用能够更好地适应不同的业务需求和数据格式。

Day02-11路径与地址栏传参

PathVariable和地址栏参数有什么区别?

特性@PathVariable查询参数 (@RequestParam)
用途用于提取 URL 路径中的数据,通常用于资源标识用于传递附加信息、过滤条件、控制请求行为
位置URL 路径的一部分URL 查询字符串中
语法/users/{userId}/users?age=25&sort=name
提取方式@PathVariable("userId") Long userId@RequestParam(name = "age", required = false) Integer age
用例GET /users/123,这里 userId123GET /users?age=25&sort=name
标准实践RESTful API 设计中,用于资源标识适用于过滤、排序、分页等操作
参数是否可选必须提供(作为路径的一部分)可选(可以设定默认值或不传)
对 URL 的影响URL 更具层次结构和语义URL 的查询字符串部分
数据传递方式通过 URL 的路径部分通过查询字符串部分
适用场景资源标识、层次结构的 URL过滤、排序、分页、附加信息

 这两处参数命名一样@PathVarible后可以不加status

Day02-13编辑员工

问题❓

为什么返回都是Controller层都返回Result.sucess()呢,不用返回Result.error()

因为全局异常类已经配置好了,业务异常会自动返回Result.error()

更新数据前判断

问题❓

有办法先判断更新数据是否与原始数据相同吗,这样能否提升性能,应该在哪个层做这个业务

在实际开发中,判断更新的数据是否与原始数据相同,主要有两个目的:

  1. 避免不必要的更新操作:如果新数据与旧数据完全相同,则不需要执行更新操作,从而减少数据库的负载。

  2. 提升性能:减少不必要的数据库操作,提高系统的整体性能。

判断数据是否相同的两种方法
方法一:在业务层(Service 层)判断
  1. 步骤

    • 在 Service 层中先通过 id 查询出原始数据。

    • 比较查询出的数据与要更新的数据,如果相同则直接返回不做任何更新操作。

  2. 代码示例

    @Service
    public class EmployeeServiceImpl implements EmployeeService {
    ​
        @Autowired
        private EmployeeMapper employeeMapper;
    ​
        @Override
        public boolean updateById(EmployeeDTO employeeDTO) {
            // 先查询原始数据
            Employee existingEmployee = employeeMapper.getById(employeeDTO.getId());
            if (existingEmployee == null) {
                throw new EntityNotFoundException("员工不存在");
            }
    ​
            // 比较原始数据与要更新的数据是否相同
            if (existingEmployee.equals(employeeDTO.toEntity())) {
                // 如果相同,直接返回不进行更新
                return false;
            }
    ​
            // 将 DTO 转换为实体对象
            Employee employee = new Employee();
            BeanUtils.copyProperties(employeeDTO, employee);
            employee.setUpdateUser(BaseContext.getCurrentId());
            employee.setUpdateTime(LocalDateTime.now());
    ​
            // 执行更新操作
            int affectedRows = employeeMapper.update(employee);
            return affectedRows > 0;
        }
    }
    • 优点:在 Service 层判断数据是否相同,可以减少不必要的数据库更新操作。

    • 缺点:需要额外的一次查询操作,这在高并发情况下可能会影响性能。

方法二:在数据库层使用 WHERE 条件
  1. 步骤

    • UPDATE 语句中添加额外的 WHERE 条件,只在新数据与旧数据不相同时才进行更新。

    • 这种方法避免了额外的查询操作,但相对复杂。

  2. SQL 语句示例

    <update id="update" parameterType="Employee">
        UPDATE employee
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="idNumber != null">id_Number = #{idNumber},</if>
            <if test="updateTime != null">update_Time = #{updateTime},</if>
            <if test="updateUser != null">update_User = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        WHERE id = #{id}
        <if test="name != null">AND name != #{name}</if>
        <if test="username != null">AND username != #{username}</if>
        <if test="password != null">AND password != #{password}</if>
        <!-- 继续为每个字段添加条件 -->
    </update>
    • 优点:减少不必要的更新操作,无需额外的查询。

    • 缺点:SQL 语句可能变得复杂且难以维护,尤其是在字段较多时。

哪个层更适合做这个业务?
  • Service 层:通常情况下,判断数据是否相同的逻辑更适合放在 Service 层,因为这是业务逻辑层,可以处理复杂的业务需求,如日志记录、事务管理等。此外,在 Service 层判断可以让你更灵活地处理各种业务场景。

  • 数据库层(Mapper 层):如果你希望最大限度地减少代码复杂性,并且能够接受稍微复杂的 SQL 语句,可以在 Mapper 层通过 WHERE 条件来判断数据是否相同。这种方法更为底层,减少了不必要的查询开销。

性能考虑
  • Service 层判断:适用于数据量较少的场景,可以通过减少不必要的数据库操作提升性能,但要注意额外查询的开销。

  • 数据库层判断:适用于需要极致性能的场景,通过减少不必要的 SQL 更新操作,最大限度地降低数据库负载。

小结

在大多数情况下,建议在 Service 层进行判断,这样可以更清晰地管理业务逻辑。如果性能是主要考虑因素且数据比较稳定,可以选择在数据库层通过复杂的 WHERE 条件来实现。

Day02-15分类模块

Category

自己写一下,锻炼一下

开发步骤

  1. 观看接口文档接口路径,创建Controller,Service,Mapper,xml并写好对应注解

  2. 观看接口文档,并逐一实现每一个接口(再次强化概念,Dto是前端传给后端的相对不完整数据,Vo是后端给前端展示附加额外东西的数据)

  3. 注意参数与返回的数据类型

  4. 没注意又犯错,业务层必须把Dto转换为Entity,因为数据库只能操作Entity

  5. 注意Java后端都是驼峰命名,不然报错,本处已改

  6. MyBatis为@Insert等提供了返回值,包括boolean或者改变的行数int,但是没有给@Select返回,所以是增删改要用对应的注解。不然用@Select就没有返回

😕疑惑:xml中的数据类型是只能Entity还是可以其他类型?

答:在 MyBatis 中,XML 文件中的 resultType 可以是实体类、DTO、Map、基本类型(如 IntegerString)等,不一定局限于实体类。虽然你在 mapper 中传递的是 DTO,但只要 DTO 和实体类在字段名称和类型上兼容,MyBatis 依然能够正常映射结果到实体类上。这就是为什么你使用 DTO 作为方法参数,而在 XML 中指定实体类 Category 作为 resultType,仍然能够正常运行的原因。

这背后的原理是 MyBatis 通过字段名称和类型来匹配查询结果与对象的属性,因此只要字段匹配,无论是 DTO 还是实体类,都能被正确映射。

Day03-01公共字段自动填充

核心知识:自定义注解,反射,aop,注解,枚举

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.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

import org.aspectj.lang.reflect.MemberSignature;
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 {

    /**
     * 切入点
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut() {

    }

    /**
     * 前置通知,在通知中进行公共字段填充
     * 还有后置通知,环绕通知
     */
    @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 {
                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);
            }
        }
    }
}
 

签名信息包含的内容

签名信息是与目标方法相关的元数据,包含了方法的结构性信息。Signature 接口和它的子接口(如 MethodSignature)可以提供以下内容:

  1. 方法名 (getName()): 获取方法的名称。

  2. 参数类型 (getParameterTypes()): 获取方法的参数类型数组。

  3. 返回类型 (getReturnType()): 获取方法的返回类型。

  4. 方法对象 (getMethod()): 获取 Method 对象,该对象表示目标方法本身,包含方法的详细信息。

  5. 方法的修饰符 (getModifiers()): 获取方法的访问修饰符(如 publicprivate)。

Method 对象的常用方法

//         获得方法上的注解对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);

先从签名

明白了,你指的是 Method 对象的常用方法,用于获取方法上的信息和操作方法。以下是一些常用的方法:

1. getAnnotation(Class<A> annotationClass)

  • 作用: 获取方法上的指定注解。

  • 示例:

    AutoFill autoFill = method.getAnnotation(AutoFill.class);
    if (autoFill != null) {
        System.out.println("注解值: " + autoFill.value());
    }

2. getAnnotations()

  • 作用: 获取方法上所有的注解。

  • 示例:

    Annotation[] annotations = method.getAnnotations();
    for (Annotation annotation : annotations) {
        System.out.println("注解: " + annotation.annotationType().getName());
    }

3. getParameterAnnotations()

  • 作用: 获取方法参数上的注解。返回一个二维数组,每个元素表示对应参数的注解。

  • 示例:

    Annotation[][] parameterAnnotations = method.getParameterAnnotations();
    for (Annotation[] annotations : parameterAnnotations) {
        for (Annotation annotation : annotations) {
            System.out.println("参数注解: " + annotation.annotationType().getName());
        }
    }

4. getReturnType()

  • 作用: 获取方法的返回类型。

  • 示例:

    Class<?> returnType = method.getReturnType();
    System.out.println("返回类型: " + returnType.getName());

5. getParameterTypes()

  • 作用: 获取方法的参数类型数组。

  • 示例:

    Class<?>[] parameterTypes = method.getParameterTypes();
    for (Class<?> paramType : parameterTypes) {
        System.out.println("参数类型: " + paramType.getName());
    }

6. getModifiers()

  • 作用: 获取方法的修饰符,如 publicprivatestatic 等。

  • 示例:

    int modifiers = method.getModifiers();
    System.out.println("修饰符: " + Modifier.toString(modifiers));

7. invoke(Object obj, Object... args)

  • 作用: 调用方法。这个方法是 Method 类的核心功能,用于在运行时动态调用方法。

  • 示例:

    method.invoke(someObject, arg1, arg2);

8. isAnnotationPresent(Class<? extends Annotation> annotationClass)

  • 作用: 检查方法上是否存在指定的注解。

  • 示例:

    boolean hasAnnotation = method.isAnnotationPresent(AutoFill.class);
    System.out.println("是否有 @AutoFill 注解: " + hasAnnotation);

9. getExceptionTypes()

  • 作用: 获取方法声明的所有异常类型。

  • 示例:

    Class<?>[] exceptionTypes = method.getExceptionTypes();
    for (Class<?> exceptionType : exceptionTypes) {
        System.out.println("异常类型: " + exceptionType.getName());
    }

10. getGenericReturnType()

  • 作用: 获取方法的返回类型的泛型信息。

  • 示例:

    Type genericReturnType = method.getGenericReturnType();
    System.out.println("泛型返回类型: " + genericReturnType.getTypeName());

这些方法允许你在运行时获取和操作方法的各种信息,非常适合用在动态代理、AOP(面向切面编程)以及其他需要反射操作的场景中。

切点方法 (autoFillPointCut)与通知方法(autoFill)

  • 切点方法通常是不包含方法体的,它仅用于标识切点。

  • 切点表达式可以

  • 通知方法可以有不同的类型,如前置通知(@Before)、后置通知(@After)、环绕通知(@Around)等。

  • 通知方法中包含了实现具体功能的代码,如日志记录、事务管理、自动填充等。

  • 在 AOP 中,定义切点方法和通知方法是实现切面功能的标准方式。这种做法提供了很好的解耦性和灵活性。

  • 切点方法 使得切点表达式可以在多个通知中重用,从而减少重复代码。

  • 通知方法 包含了具体的业务逻辑,能够在切点匹配时被执行。

这里写的是否不严谨,如果是批量修改(参数为List<Obejct>的话如何处理),那就需要一个循环为数组里的所有对象添加公共字段

Day03-05新增菜品需求分析与设计

接口分析与设计

思考需要几个接口?

3个

根据上述原型图先粗粒度设计接口,共包含3个接口。

  1. 新增之前需要查询分类,设置分类,查询口味,设置口味

  2. 菜品有图片,文件上传的接口

  3. 发送post新增请求

主键已经在数据库设计好了自增

dish菜品表

重要属性

category_id 分类id 逻辑外键

dish_flavor菜品口味表

重要属性

dish_id 菜品id 逻辑外键

每个菜品都可以拥有多种口味,而每个口味又可以被多种菜品选择,所以是多对多(互相选择)

但是在黑马设计的数据库中体现出来的是一对多关系

照理来说,还应该有一个flavor表,为以下这样:

idnamevalue1value2value3value4
1甜味无糖3分糖半糖全糖
2温度热饮常温少冰正常冰
3忌口不要葱不要香菜不要蒜不要醋

Day03-06新增菜品代码开发1

阿里Oss使用

创建自己的桶,设置为私有,设置访问条件为固定ip

在common已经提供配置属性类,作用是在写yml配置时有提示,以免写错

在yml配置文件里,规范写法使用 - 进行分割单词,但直接写accessKeyId也是没有问题的

人称火车模式

规范开发需要dev的yml和prod的yml设置数据库,OSS等可能会变的数据

📜tips:就我们自己玩苍穹外卖那点流量开阿里云OSS是不要钱的

Day03-08新增菜品代码开发

再次熟悉开发流程

  1. 查看接口文档,创建DishController

  2. 按照接口文档要求写post新增菜品接口

  3. 核心代码,DishServiceImpl内实现

    1. useGeneratedKeyskeyProperty获得插入数据生成的主键值,固定语法

    2. <insert id="insert" useGeneratedKeys="true" keyProperty="id">
          insert into dish (name, category_id, price, image, description,status,
                            create_time, update_time, create_user, update_user) VALUE
                  (#{name},#{categoryId},#{price},#{image},#{description},{status},
                           #{createTime},#{updateTime},#{createUser},#{updateUser})
      </insert>
    3. tips:这里用MP就可以直接获取ID,调用insert时MP已经帮我们set了

  4. 然后再到Service内就可以获取dishid

注意<foreach>标签内的拼写

Day03-11分页查询菜品代码开发

虽然是多表查询,但是也比较简单

    @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {

        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
//      为前端封装VO,在这里就是为菜品封装口味发送给前端
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
        long total = page.getTotal();
        List<DishVO> result = page.getResult();

        return new PageResult(total, result);

    }

 sql使用左连接保证菜品不为null

<!--菜品分页查询-->
    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        SELECT d.*,c.`name` AS categoryName FROM dish AS d LEFT OUTER JOIN category AS c ON d.category_id = c.id
        <where>
            <if test="name != null">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and d.category_id like concat('%',#{categoryId },'%')
            </if>
            <if test="status != null">
                and d.status like concat('%',#{status},'%')
            </if>
        </where>
        order by d.create_time desc
    </select>

Day03-12删除菜品需求分析

业务规则:

  1. 可以一次删除一个菜品,也可以批量删除菜品

  2. 起售中的菜品不能删除

  3. 被套餐关联的菜品不能删除

  4. 删除菜品后,关联的口味数据也需要删除掉

涉及到的表操作

批量查询

1、判断菜品是否能够删除--是否起售

逻辑分析:只要ids内有一个是起售状态,则抛出异常

即匹配任意元素返回

可以先批量查询,然后拿到结果集,使用java8的stream中的anyMatch来做

使用anyMatch

//        使用 Stream API 判断是否有菜品的状态为 0,如果有则抛出异常
if (batchById.stream().anyMatch(dish -> dish.getStatus() == 0)) {
    throw new RuntimeException("菜品状态为0,无法删除");
}

使用filters和findany

//        使用 Stream API 遍历列表,判断 status 是否为 0,如果是则抛出异常
batchById.stream()
        .filter(dish -> dish.getStatus() == 0)
        .findAny()
        .ifPresent(dish -> {
            throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
        });

findany和anymatch两种方法哪个更好

findAny()anyMatch() 是 Stream API 中的两个不同方法,用途也不同:

  • findAny()findAny() 方法返回流中的任意一个元素,它通常用于并行流,因为在并行流中,元素的顺序不确定。如果您只是需要找到流中的任意一个元素,而不在乎具体是哪个元素,可以使用 findAny()

  • anyMatch()anyMatch() 方法用于检查流中是否有任意一个元素匹配给定条件。它返回一个 boolean 值,表示是否至少有一个元素匹配条件。这个方法更适合用于判断流中是否存在符合条件的元素。

因此,要根据具体的需求来选择使用哪个方法。只是需要找到流中的任意一个元素,可以使用 findAny();如果需要检查流中是否有任意一个元素匹配给定条件,可以使用 anyMatch()

2、判断菜品是否能够删除--是否与套餐关联

逻辑分析:还是批量查询,查询出有数据则说明有关联,不能删除

//        2、判断菜品是否能够删除--是否与套餐关联
        List<Long> sdIdsByDishIds = setmealDishMapper.getSDIdsByDishIds(ids);
//        不为空,而且还大于0说明套餐还有关联
        if (sdIdsByDishIds != null && sdIdsByDishIds.size() >= 0){
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
        }

这里我做了一些优化,向用户返回具体的异常数据,可以提示用户,更加友好 

  /**
     * 菜品批量删除
     *
     * @param ids 菜品id列表
     * @return 删除多少行
     */
    @Override
    @Transactional
    public Integer deleteBatch(List<Long> ids) {

//        1、判断菜品是否能够删除--是否起售
        List<Dish> dishList = dishMapper.getBatchById(ids);
//        防止出现不存在的id
        if (dishList.size() != ids.size()) {
            throw new DeletionNotAllowedException("删除的菜品中有" + MessageConstant.ACCOUNT_NOT_FOUND);
        }
//        使用 Stream API 遍历列表,判断 status 是否为 1,如果是则抛出异常
//        并提示起售菜品
        List<String> dishesOnsale = dishList.stream()
                .filter(dish -> dish.getStatus() == StatusConstant.ENABLE)
                .map(Dish::getName)
                .collect(Collectors.toList());
        if (dishesOnsale.size() > 0) {
            String dishes = String.join(",", dishesOnsale);
            throw new DeletionNotAllowedException(dishes+MessageConstant.DISH_ON_SALE);
        }
//        2、判断菜品是否能够删除--是否与套餐关联
//        并提示被关联套餐
        List<Long> setmealIds = setmealDishMapper.getSDIdsByDishIds(ids);
//        不为空,而且还大于0说明套餐还有关联
        if (setmealIds != null && setmealIds.size() > 0) {
//            把有关联得套餐取出来展示给用户
            List<Setmeal> setmeals = setmealMapper.getBatchByIds(setmealIds);
            List<String> setMealNames = setmeals.stream()
                    .map(Setmeal::getName)
                    .collect(Collectors.toList());
            String s = String.join(",", setMealNames);
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL+s);
        }
//        批量删除口味
        dishFlavorMapper.deleteBatchByDishIds(ids);
//        批量删除菜品
        Integer deleteBatchByIds = dishMapper.deleteBatchByIds(ids);
        if (deleteBatchByIds != ids.size()) {
            throw new DeletionNotAllowedException(MessageConstant.UNKNOWN_ERROR);
        }
        return deleteBatchByIds;
    }

🚧循环执行sql问题

循环执行 SQL:

  • 效率考虑

    • 优点:可以根据具体需求灵活控制每次执行的 SQL 语句,可以更好地控制事务边界。

    • 缺点:可能会引入较多的数据库交互,增加数据库连接、SQL 解析和执行的开销,影响性能。

  • 规范性考虑

    • 优点:可以更加直观地控制 SQL 的执行过程,便于维护和调试。

    • 缺点:如果循环执行 SQL 操作频繁且数据量大,可能会导致代码冗余,不利于代码的整洁性和可维护性。

在 XML 中使用 foreach 批量执行 SQL:

  • 效率考虑

    • 优点foreach 可以将多个参数一次性传入 SQL,减少了数据库连接和 SQL 解析的开销,提高了性能。

    • 缺点:可能会导致 SQL 语句的复杂性增加,不够灵活,难以动态控制每次执行的 SQL。

  • 规范性考虑

    • 优点:将批量操作的逻辑集中在 XML 中,减少了代码冗余,提高了代码的整洁性和可维护性。

    • 缺点:如果操作逻辑过于复杂,可能会使 XML 文件变得臃肿,不利于维护。

在一般情况下,推荐在开发中使用在 XML 中使用 foreach 批量执行 SQL 的方式,因为它可以提高性能并且使代码更加整洁和易维护。然而,如果涉及到复杂的逻辑控制或需要灵活性更高的 SQL 执行方式,循环执行 SQL 也是一种可行的选择。最终选择哪种方式应根据具体业务需求、性能要求和代码维护的考虑来决定。

Day03-13修改菜品

因为数据库和前端代码设计好了,

所以修改时只能把原有的口味全部清除后再插入新的

📜tips:在这里我增加了判断状态,是否起售,只有停售才能修改,为后面缓存做准备

    /**
     * 修改菜品及其口味
     *
     * @param dishDTO
     * @return
     */
    @Override
    @Transactional
    public Integer updateWithFlavors(DishDTO dishDTO) {
//        判断菜品是否已经停售否则不能修改
        if (dishDTO.getStatus() == StatusConstant.ENABLE) {
            throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
        }
//      1.封装dish
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO, dish);
        Integer update = dishMapper.update(dish);
//      2.获取dishFlavors
        List<DishFlavor> flavors = dishDTO.getFlavors();
        Long dishId = dish.getId();
//        TODO 修改数据库,让菜品和口味形成多对多关系,才能修改

//          删除原来所有口味,然后插入新口味
        dishFlavorMapper.deleteByDishId(dishId);
        flavors.stream().forEach(flavor -> {
//            取出每个flavor并修改
            flavor.setDishId(dishId);
            dishFlavorMapper.insertByDishId(flavor);
        });
        return update;
    }

Day04套餐所有接口

除了分页查询,本质都是单表操作,和菜品是前面一样的

需求分析:返回套餐分类下的所有套餐数据即可,不需要套餐内的数据

<!--    套餐分页查询,都是用模糊查询-->
<select id="pageQuery" resultType="com.sky.vo.SetmealVO" parameterType="com.sky.dto.SetmealPageQueryDTO">
    select s.*,c.name as categoryName from setmeal as s left join category as c on s.category_id = c.id
    <where>
        <if test="name != null">
            and s.name like concat('%',#{name},'%')
        </if>
        <if test="status != null">
            and s.status like concat('%',#{status},'%')
        </if>
        <if test="categoryId != null">
            and s.category_id = #{categoryId}
        </if>
    </where>
    order by s.create_time desc
</select>

修改套餐之前也要判断是否停售

    /**
     * 修改套餐
     *
     * @param setmealDTO
     * @return 修改行数
     */
    @Override
    @Transactional
    public Integer update(SetmealDTO setmealDTO) {
        log.info("修改套餐信息,参数为:{}", setmealDTO);
        //        判断套餐是否已经停售否则不能修改
        if (setmealDTO.getStatus() == StatusConstant.ENABLE) {
            throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
        }
//        提取Dto中setmeal对象
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);
        setmealMapper.update(setmeal);

        Long setmealId = setmealDTO.getId();
//      因为可能有新增操作,所以先删除再插入
        setmealDishMapper.deleteBySetmealId(setmealId);

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        return setmealDishMapper.saveBatch(setmealDishes);
    }

 多对多关系,两次查询,一次查对应关联表(setmeal_dish),一次查对应实体表(dish)

请求参数分析

参数为路径参数id,需要加@PathVariable注解

响应结果分析

响应结构为

{
    "code": 0,
    "data": {
        "categoryId": 0,
        "categoryName": "string",
        "description": "string",
        "id": 0,
        "image": "string",
        "name": "string",
        "price": 0,
        "setmealDishes": [
            {
                "copies": 0,
                "dishId": 0,
                "id": 0,
                "name": "string",
                "price": 0,
                "setmealId": 0
            }
        ],
        "status": 0,
        "updateTime": "2019-08-24T14:15:22Z"
    },
    "msg": "string"
}

返回SetmealVO类型

  /**
     * 根据套餐id查询菜品选项
     *
     * @param id
     * @return
     */
    @Override
    public List<DishItemVO> getDishItemById(Long id) {
        log.info("根据套餐id查询菜品选项,id为{}", id);
//        在mapper查询出套餐-菜品对应数据
        List<SetmealDish> setmealDishes = setmealDishMapper.getBySetmealId(id);

        if (setmealDishes == null || setmealDishes.size() == 0) {
            throw new RuntimeException("该套餐暂无数据,请联系管理员");
        }

//        name和copies封装
        List<DishItemVO> dishItemVOList = setmealDishes.stream().map(setmealDish ->
                DishItemVO.builder()
                        .name(setmealDish.getName())
                        .copies(setmealDish.getCopies())
                        .build()
        ).collect(Collectors.toList());

//        根据dishIds查询需要的菜品数据并封装
        List<Long> dishIds = setmealDishes.stream()
                .map(SetmealDish::getDishId)
                .collect(Collectors.toList());
        List<Dish> dishes = dishMapper.getBatchById(dishIds);
//        封装image和description
        dishItemVOList.forEach(dishItemVO -> {
            Optional<Dish> matchingDish = dishes.stream()
                    .filter(dish -> dish.getName().equals(dishItemVO.getName()))
                    .findFirst();

            matchingDish.ifPresent(dish -> {
                dishItemVO.setImage(dish.getImage());
                dishItemVO.setDescription(dish.getDescription());
            });
        });
        return dishItemVOList;
    }

📜tips:关联表不设置创建人,创建时间,修改人,修改时间

Day05-11Redis

Redis is an open source (BSD licensed), in-memory data structure store, used as a database, cache, and message broker,翻译为:Redis是一个开源的内存中的数据结构存储系统,它可以用作︰数据库、缓存和消息中间件。官网: Redis - The Real-time Data Platform Redis是用C语言开发的一个开源的高性能键值对(key-value)数据库,官方提供的数据是可以达到100000+的QPS(每秒内查询次数)。它存储的value类型比较丰富,也被称为结构化的NoSql数据库。 NoSql (Not only sQL),不仅仅是SQL,泛指非关系型数据库。NoSql数据库并不是要取代关系型数据库,而是关系型数据库的补充。

Redis中文网

关系型数据库(RDBMS)

  • Mysql

  • Oracle>DB2

  • SQLServer

非关系型数据库(NoSql)

  • Redis

  • Mongo db

  • MemCached

Redis应用场景

  • 缓存任务

  • 队列

  • 消息队列

  • 分布式锁

常用的Redis框架

  • Jedis

  • Lettuce

  • Spring Data Redis

RedisTemplate和StringRedisTemplate

在Spring框架中,RedisTemplateStringRedisTemplate是用于与Redis数据库进行交互的两个重要类。它们之间的主要区别在于它们处理数据的方式和支持的数据类型。

  1. RedisTemplate:

    • RedisTemplate是Spring提供的用于与Redis进行交互的通用模板类。

    • 它是一个泛型类,可以处理任意类型的数据,包括字符串、列表、集合、散列、有序集合等。

    • RedisTemplate需要设置SerializationDeserialization的策略,以便将Java对象序列化为Redis存储的数据,并将从Redis中读取的数据反序列化为Java对象。

    • RedisTemplate支持更丰富的Redis数据类型和操作,但使用起来相对复杂一些。

  2. StringRedisTemplate:

    • StringRedisTemplateRedisTemplate的子类,专门用于处理字符串类型的数据。

    • StringRedisTemplate已经配置了默认的String类型的序列化和反序列化策略,因此在处理字符串类型数据时更为方便。

    • 由于StringRedisTemplate专注于处理字符串类型的数据,因此它的方法更为简单直接,更适合于处理简单的键值对数据。

选择使用哪一个

  • 如果需要处理除字符串之外的其他数据类型,或者需要更多高级的Redis功能,应该选择RedisTemplate

  • 如果只需要处理字符串类型的数据,且希望使用更简单的API,那么StringRedisTemplate可能更适合。

RedisTemplate操作类常用方法

当RedisTemplate与 Redis 进行交互时,以下是 ValueOperationsListOperationsSetOperationsHashOperationsZSetOperations 这五个操作类中的常用和重要方法:

ValueOperations

  • set(key, value): 设置指定 key 的值。

  • get(key): 获取指定 key 的值。

  • increment(key, delta): 将 key 的值增加 delta。

  • decrement(key, delta): 将 key 的值减少 delta。

ListOperations

  • leftPush(key, value): 在列表左侧插入值。

  • rightPush(key, value): 在列表右侧插入值。

  • range(key, start, end): 获取列表指定范围内的值。

  • trim(key, start, end): 修剪列表,保留指定范围内的值。

SetOperations

  • add(key, values): 向集合添加一个或多个值。

  • isMember(key, value): 检查值是否是集合的成员。

  • members(key): 获取集合中的所有成员。

  • difference(key1, key2): 返回集合 key1 中存在而集合 key2 中不存在的成员。

HashOperations

  • put(key, hashKey, value): 设置哈希表中指定 key 的值。

  • get(key, hashKey): 获取哈希表中指定 key 的值。

  • keys(key): 获取哈希表中所有的 key 值。

  • entries(key): 获取哈希表中所有的键值对。

ZSetOperations

  • add(key, value, score): 向有序集合添加一个成员,同时指定分数。

  • range(key, start, end): 获取有序集合指定范围内的成员,默认按分数从小到大排序。

  • reverseRange(key, start, end): 获取有序集合指定范围内的成员,按分数从大到小排序。

  • rangeByScore(key, min, max): 根据分数范围获取有序集合的成员。

  • rank(key, value): 获取成员在有序集合中的排名,按分数从小到大排序。

  • reverseRank(key, value): 获取成员在有序集合中的排名,按分数从大到小排序。

  • incrementScore(key, value, delta): 增加成员的分数。

  • remove(key, values): 移除有序集合中的一个或多个成员。

Day05-15店铺状态

使用Spring Data Redis 开发

Redis 存储店铺状态的好处:

  1. 快速访问

    • Redis 的高性能和快速访问特性适合存储需要频繁访问的数据,如店铺状态。这样可以提高系统的响应速度。

  2. 临时性数据

    • 如果店铺状态是临时性的、不需要长期保存的数据,直接存储在 Redis 中可以避免对数据库的频繁读写,提高性能。

  3. 缓存

    • Redis 的缓存特性可以帮助减轻数据库的压力,提高系统整体性能。

@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 修改店铺状态
     * @param status
     * @return
     */
    @PutMapping("{status}")
    @ApiOperation("修改店铺状态")
    public Result setStatus(@PathVariable Integer status) {
        log.info("设置店铺状态为:{}", status == 1 ? "营业中" : "打烊中");
//      使用redis存储
        redisTemplate.opsForValue().set(MessageConstant.Shop_Status, status);
        return Result.success();
    }

    /**
     * 获取店铺状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺状态")
    public Result<Integer> getStatus() {
        Integer status = (Integer) redisTemplate.opsForValue().get(MessageConstant.Shop_Status);
        log.info("获取店铺状态为:{}", status == 1 ? "营业中" : "打烊中");
        return Result.success(status);
    }

Day06-05~12微信小程序入门

HttpClient

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

HttpClient的maven坐标:

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

HttpClient的核心API:

  • HttpClient:Http客户端对象类型,使用该类型对象可发起Http请求。

  • HttpClients:可认为是构建器,可创建HttpClient对象。

  • CloseableHttpClient:实现类,实现了HttpClient接口。

  • HttpGet:Get方式请求类型。

  • HttpPost:Post方式请求类型。

HttpClient发送请求步骤:

  • 创建HttpClient对象

  • 创建Http请求对象

  • 调用HttpClient的execute方法发送请求

Day06-14微信登录

个人开发小程序不能获取用户手机号

微信登录业务层设计与分析

  1. 调用微信的接口服务,获得当前用户的openid

  2. 判断openid是否为空

    1. openid为空,抛出异常

    2. 不为空,判断当前用户是否为新用户

      1. 是新用户,自动完成注册,封装user对象存入数据库并返回

      2. 不是新用户,查询数据库对应用户并返回

登录测试成功,返回了数据

{"code":1,"msg":null,"data":{"id":1,"openid":"oK0ST67FECmTNZqeWIKCZ_AomUg4","token":"eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE3MjU3ODgyMzIsInVzZXJJZCI6MX0.QQ8Du4y2IDSPNgocFqMl5imQfWHZDGD4-mm4Z8-eZcs"}}

Day06-18登录令牌校验

1.创建自定义拦截器,即JwtTokenUserInterceptor,获取令牌,检验令牌

继承自HandlerInterceptor

package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenUserInterceptor 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 {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌(用户)
        String token = request.getHeader(jwtProperties.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前用户id:{}", userId);
//          存储用户id进入ThreadLocal
            BaseContext.setCurrentId(userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

Day06-19C端-接口导入

实现接口

  1. C端-套餐浏览接口

  2. C端-菜品浏览接口

  3. C端-分类接口

注意根据套餐id查询包含的菜品列表需要封装DishItemVO

需要从两张表获取数据

1.setmeal_dish 封装name和copies

2.dish image和description

在使用apifox进行客户端访问接口测试时要注意user的token名字

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值