苍穹外卖学习记录

苍穹外卖学习

文章目录

知识前提:

Nginx

前端请求通过Nginx转发到后端服务器,从而实现反向代理。

Swagger

  1. 在配置类中加入 knife4j 相关配置

    WebMvcConfiguration.java

    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;
        }
  2. 设置静态资源映射,否则接口文档页面无法访问

    WebMvcConfiguration.java

    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/");
    }



1.管理员登录

思路:

  1. 前端发起登录请求:前端通过 HTTP 请求向后端发送登录信息,包括用户名和密码。
  2. 后端接收并处理请求 :后端接收请求,将前端传来的数据封装成 EmployeeLoginDTO 对象,并进行登录校验。
  3. 业务逻辑处理:在业务层进行具体的登录逻辑处理,包括数据库查询和密码校验。
  4. 生成 JWT 令牌 :登录成功后,生成 JWT 令牌,并将其与管理员信息一起封装成 EmployeeLoginVO 对象。
  5. 响应结果 :将封装好的 EmployeeLoginVO 对象作为响应返回给前端。

详细步骤:

  1. 前端发起登录请求
    • 前端通过 POST 请求向后端发送管理员的登录信息(用户名和密码)。
  2. 后端接收并处理请求
    • 控制层接收前端传来的登录信息,并将其封装成 EmployeeLoginDTO 对象。
    • 调用业务层的登录方法进行校验。

MD5加密:password = DigestUtils.md5DigestAsHex(password.getBytes()); //MD5加密

业务逻辑处理

  • 在业务层通过用户名查询数据库
  • 判断用户名是否存在或锁定
  • 接着进行密码校验,校验完毕后,成功返回管理员对象,否则报异常(用户名不存在、密码不对、账号被锁定)


生成 JWT 令牌

  • 此时验证成功后,生成包含管理员信息的 JWT 令牌,格式是Map键值对

  • 通过yaml文件的配置生成JWT令牌。通常 JWT 令牌会包含管理员的 ID 和一些其他必要的信息。


封装响应对象

  • 将管理员信息和 JWT 令牌封装进 EmployeeLoginVO 对象中。
  • EmployeeLoginVO 类实现了 Serializable 接口,确保对象可以被序列化。
  • 使用 Lombok 的 @Data@Builder 等注解简化对象的创建和使用。

这里VO对象封装是使用了构造器模式和序列化接口等技术,使对象的创建和使用更方便高效。

1.1新增员工


问题1:在新增员工时,需要将当前登录人的id存入表中对应的创建人id和修改人id。

可以通过ThreadLocal进行单一的线程存储,存储的id实在JWT令牌解析后,直接存进Threadlocal,因为线程都是一致的,所以可以直接使用。

问题2:录入的用户名已存在时,抛出异常后并未处理,直接停止服务了。

这里可以看出,报错的异常名为SQLIntegrityConstraintViolationException

Duplicate entry 'zhangsan' for key 'employee.idx_username' 意思就是重复的用户名,所以就需要设置自定义异常

1.2员工信息分页查询


注意事项:

  • 请求参数类型为Query,不是json格式提交,在路径后直接拼接。/admin/employee/page?name=zhangsan
  • 返回数据中records数组中使用Employee实体类对属性进行封装。

思考:

  • 通过PageHelper分页插件进行分页查询,在业务层方面使用前端传入的DTO对象,将当前页码和每页的大小传入分页插件,通过Mapper进行分页查询后,需要将指定数据传回前端。
  • 例如Total(总记录数/总页数)和Records(当前页数据集合),此时page中已经完成分页查询并动态计算出当前页面数据和总页数记录,只需get、set即可传入PageResult
  • PageResult是一个封装分页查询结果的类

问题1:分页查询时,Records中的时间与前端需要的不符


  • 方法1:通过@JsonFormat进行时间格式规范化
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
  • 方法2:通过在WebMvcConfiguration中扩展SpringMVC的消息转换器,统一对日期类型进行格式处理
java 复制代码
	/**
     * 扩展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);
    }

这里的JacksonObjectMapper是一个对象映射器,通过规范化定义时间格式确保日期和时间以特定的格式进行序列化和反序列化


知识点:序列化和反序列化

在这个场景中,序列化和反序列化指的是将 Java 对象转换为 JSON 格式的字符串(序列化),以及将 JSON 格式的字符串转换回 Java 对象(反序列化)的过程。

序列化 (Serialization)

  • 序列化是将 Java 对象的状态信息转换为可以存储或传输的形式的过程。在这个例子中,序列化是将 Java 对象转换为 JSON 格式的字符串。
  • 例如,如果你有一个 Employee 类,序列化会将 Employee 对象的信息转换为一个 JSON 字符串,如下所示:
java 复制代码
Employee employee = new Employee("John Doe", 30, LocalDate.now());

// 序列化为 JSON
String json = objectMapper.writeValueAsString(employee);
System.out.println(json);

输出的 JSON 字符串可能会类似于:

java 复制代码
{
  "name": "John Doe",
  "age": 30,
  "birthdate": "2024-08-14"
}

反序列化 (Deserialization)

反序列化是从序列化的表示形式中提取数据,并重新创建 Java 对象的过程。在这个例子中,反序列化是从 JSON 字符串中恢复出原始的 Java 对象。例如:

String json = "{"name":"John Doe","age":30,"birthdate":"2024-08-14"}";

java 复制代码
// 反序列化为 Java 对象
Employee employee = objectMapper.readValue(json, Employee.class);
System.out.println(employee.getName());

这将输出:

java 复制代码
John Doe

总结
序列化 :将 Java 对象转换为 JSON 字符串。反序列化 :将 JSON 字符串转换回 Java 对象。
自定义序列化器和反序列化器:通过 SimpleModule 添加自定义逻辑来处理特定类型的序列化和反序列化,例如日期和时间类型。

1.3修改员工时信息回显

通过点击当前员工修改按钮,获取该员工id,通过id查询数据库


1.4 修改员工信息



这里可以使用先前启用禁用员工账号 的更新方法,万金油,通过参数值是null来排除修改的值,有值才会更修改,null直接跳过


省略类似的CRUD功能


2.删除分类注意事项

  • 在删除分类之前判断是否有关联的菜品或套餐是为了保证数据的一致性和完整性。
  • 如果直接删除一个被其他数据引用的分类,可能会导致数据库中的数据丢失或者出现"悬挂"引用的情况,即某些记录失去了与之相关的分类信息。

在实际应用中,这样的逻辑是非常重要的,因为它可以帮助开发者确保应用程序的数据一致性。例如,在餐饮管理系统中,如果一个分类被菜品或套餐引用,那么删除该分类可能会导致用户无法查看到这些菜品或套餐所属的分类信息,从而影响用户体验和系统的可靠性。因此,在删除分类之前进行这样的检查是非常必要的。

2.1公共字段自动填充

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

实现思路

在实现公共字段自动填充,也就是在插入或者更新的时候为指定字段赋予指定的值,使用它的好处就是可以统一对这些字段进行处理,避免了重复代码。在上述的问题分析中,我们提到有四个公共字段,需要在新增/更新中进行赋值操作, 具体情况如下:

序号 字段名 含义 数据类型 操作类型
1 create_time 创建时间 datetime insert
2 create_user 创建人id bigint insert
3 update_time 修改时间 datetime insert、update
4 update_user 修改人id bigint insert、update

实现步骤:

1). 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法

2). 自定义切面类 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值

java 复制代码
@Slf4j
@Aspect //定义一个切面
@Component
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){
            try {
                //为4个公共字段赋值
                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){
            try {
                //为2个公共字段赋值
                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();;
            }

        }

    }
}

理解

  • 通过给 AutoFillAspect 添加注解 @Aspect 说明这是一个切面类,这个 @Pointcut 是一个切入点,也就是作为拦截的,而 execution (定义切入点表达式)是设置拦截的范围是 com.sky.mapper 包下的所有类和所有方法, @annotation(com.sky.annotation.AutoFill) 表示它只拦截带有 AutoFill 注解类型的方法。
  • 此时,因为公共字段填充字段是需要在修改字段或者新增字段之前完成填充,所以是前置通知 @before , @Before("autoFillPointCut()") 这个参数就是告诉你,这个前置通知的方法是需要找范围和要求的,而这个范围就是这个切入点的范围和要求。
  • 通过执行 autoFill 方法,这里参数是 joinPoint ,我认为这是获取的一个切入点,通过 getSignature 获取当前切入点的方法签名,例如 update 方法,这只是方法签名,想要真正获取到方法上的注解,就需要进一步获取到该方法上的注解,所以使用 getMethod 以及 getAnnotation 去指定获取 AutoFill 自定义注解。
  • 接下来获取到这个方法上的 AutoFill 注解了,但是我只需要这个数据库操作参数,所以我们可以发现它的 value 值正好就是数据库操作类型, autoFill 是以键值对存储这个操作类型的。
  • 通过 getArgs 获取 joinPoint 的实体对象,此时这个实体对象其实就是前端获取到你输入的字段,比如新增的字段或者修改的字段,把它存进 args 里面,判断它非空且长度非零,然后将第一个元素赋给一个 Object 类的实体(其实里头最多就一个元素因为不考虑并发),然后进行准备赋值数据,使用 LocalDateTime.now() 和 BaseContext.getCurrentId() 来获取当前时间和当前操作人的ID。
  • 判断操作类型,通过先前获取的数据库操作类型与 insert 和 update 进行比对。例子中是 update ,所以直接进入 else if 。这里通过反射机制获取实体类中的方法:自己定义一个 Method 对象 setUpdateTime ,这个对象代表 entity 对象内的一个方法。
  • 这个方法里面有两个参数: AutoFillConstant.SET_UPDATE_TIME 表示方法的名字,即 setUpdateTime 。 LocalDateTime.class 表示这个方法接受一个 LocalDateTime 类型的参数。通过 entity.getClass() 获取当前运行的类。再通过 getDeclaredMethod 方法,我们获取到了当前实体类中声明的方法 setUpdateTime 。
  • setUpdateTime.invoke(entity, now) 反射机制就反着看, invoke 方法允许我们在运行时动态地调用对象的方法。我们实际上是调用了 entity 对象的 setUpdateTime 方法,并将当前时间 now 作为参数传递给这个方法。

3). 在 Mapper 的方法上加入 AutoFill 注解

2.2文件上传功能(阿里云OSS存储)


配置yaml文件,配置阿里云OSS的相关属性,包括endpoint、accessKeyId、accessKeySecret和bucketName,注意区分开发环境和生产环境的配置差异。创建AliOssProperties类,用@ConfigurationProperties注解来自动读取这些配置以便后期用

其次,创建一个oss对象配置类,用来返回一个文件上传对象,这个对象里面存储了阿里云对应配置的属性。

接着创建一个AliOssUtil工具类,里面有一个upload方法里面俩参数:字节数组和目标文件名,只需要搞懂,这个方法可以创建一个ossclient实例,将oss属性参数存进去,因为存储路径由https://BucketName.Endpoint/ObjectName组成的,所以通过bucketName和endpoint以及最后的objectName获取到文件访问路径。

最后,在控制层进行上传业务,通过对应接口mapping进行上传操作,需要注意,在上传文件之前,需要注意文件名需要保证唯一性,通常的做法是使用UUID生成新的文件名,截取文件的后缀名,并保留原有的文件扩展名最后进行upload操作。

业务流程:

  • MultipartFile对象中读取文件数据,使用getBytes()方法获取文件的字节流。
    生成唯一文件名:截取文件原始名称的扩展名,通常使用UUID来保证唯一性,生成一个新的文件名。
  • 调用aliOssUtil.upload(file.getBytes(), name)方法上传文件。这里file.getBytes()获取文件字节流,name是新生成的唯一文件名。
  • 上传成功,返回文件的访问路径,路径由https://BucketName.Endpoint/ObjectName组成。

2.3新增菜品功能注意

思考:初步分析新增菜品分为三部分:文件上传、新增菜品和新增菜品口味,所以新增的是两个表的数据

业务层逻辑是向菜品表和口味表插入数据,其中需要注意:

  • DTO的类型转换和数据拷贝,插入菜品表是一行数据一行数据插入。
  • 口味是有多种口味在一个属性内,就需要插入n条数据并判断非空。
  • 口味表的dish_id就是菜品表的id,这里通过数据库的自动生成策略获取新插入菜品的ID,再将此ID设置到每个口味项的dishId上,并批量插入口味数据到dish_flavor表。
  • 使用了@Transactional注解保证事务一致性,即菜品和口味的插入操作要么全部成功,要么全部失败。


DishMapper接口定义了插入菜品数据的方法,并使用@AutoFill注解自动填充创建时间和更新时间等字段。对应XML文件提供了SQL语句实现插入操作,并启用useGeneratedKeys返回自动生成的主键。

  • useGeneratedKeys="true": 意思是Mybatis在执行完插入操作后,使用数据库自动生成的主键值填充返回的对象。
  • keyProperty="id": 指定将自动生成的主键值填充到对象的哪个属性中。在这个例子中,id 是对象的主键属性名称。


DishFlavorMapper接口用于操作口味数据,定义了批量插入口味数据的方法,其XML文件中通过foreach实现了一次性插入多条口味记录的功能。这里的separator表示分隔符为逗号,collection 属性用于指定集合名称,item表示集合中的每一个元素的别名。

DTO/VO/DO/PO理解:



通俗的解释:领域模型中的实体类分为四种模型:VO、DTO、DO和PO

VO:(View Object):视图对象,用于展示层

DTO(Data Transfer Object):数据传输对象 在后端,它的存在形式是java对象,也就是在controller里面定义的请求参数

DO(Domain Object):领域对象 ,它用来接收数据库对应的实体,是一种抽象化的数据状态,介于数据库与业务逻辑之间

PO(Persistent Object):持久化对象 ,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系。

BO (Business Object) :业务对象 BO通常位于中间层或业务逻辑层。BO支持序列化和反序列化,用来是把业务逻辑封装为一个对象

方向:后端-->前端

VO:前端页面显示使用的数据,是后端传递给前端的。

方向:前端-->后端

DTO:前端调用后端接口的时候传递给后端

DO:controller中接收到DTO之后,新建一个DO传递给service,

PO:service接收到传递的DO之后,转换成一个PO,传给mapper的方法,进行持久化处理。

BO:用于微服务之间传输数据

2.4分页查询sql

左外连接:返回的是VO对象,用到了左外连接,连接条件是将category表中id字段等同为dish表中的category_id字段,并根据VO对象属性参数查询dish表和category的name字段。

动态 SQL:使用 标签动态构建 WHERE 子句,根据传入的参数决定是否添加过滤条件。

2.5删除菜品

业务规则:

  • 可以一次删除一个菜品,也可以批量删除菜品
  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除掉
4.1.3 表设计

在进行删除菜品操作时,会涉及到以下三张表。


注意事项:

  • 在dish表中删除菜品基本数据时,同时,也要把关联在dish_flavor表中的数据一块删除。
  • setmeal_dish表为菜品和套餐关联的中间表。
  • 若删除的菜品数据关联着某个套餐,此时,删除失败。
  • 若要删除套餐关联的菜品数据,先解除两者关联,再对菜品进行删除。



删除代码优化:


2.5新增菜品接口

1.根据id查询菜品:

2.修改菜品

3.新增套餐

接口设计(共涉及到4个接口):

  • 根据类型查询分类(已完成)
  • 根据分类id查询菜品
  • 图片上传(已完成)
  • 新增套餐

业务规则:

  • 套餐名称唯一
  • 套餐必须属于某个分类
  • 套餐必须包含菜品
  • 名称、分类、价格、图片为必填项
  • 添加菜品窗口需要根据分类类型来展示菜品
  • 新增的套餐默认为停售状态



4.SpringCache缓存


其中store是cacheManager缓存,默认存储的一个HashMap类型数据

相关推荐
Reese_Cool11 分钟前
【数据结构与算法】排序
java·c语言·开发语言·数据结构·c++·算法·排序算法
颜淡慕潇1 小时前
【K8S系列】kubectl describe pod显示ImagePullBackOff,如何进一步排查?
后端·云原生·容器·kubernetes
TheITSea1 小时前
云服务器宝塔安装静态网页 WordPress、VuePress流程记录
java·服务器·数据库
AuroraI'ncoding1 小时前
SpringMVC接收请求参数
java
Hacker_Oldv1 小时前
网络安全的学习路线
学习·安全·web安全
蒟蒻的贤1 小时前
vue学习11.21
javascript·vue.js·学习
高 朗1 小时前
【GO基础学习】基础语法(2)切片slice
开发语言·学习·golang·slice
Clarify1 小时前
docker部署go游戏服务器(进阶版)
后端
九圣残炎1 小时前
【从零开始的LeetCode-算法】3354. 使数组元素等于零
java·算法·leetcode
寒笙LED2 小时前
C++详细笔记(六)string库
开发语言·c++·笔记